From ce7c4487e78853c7a4265d3e3de16e140e49c598 Mon Sep 17 00:00:00 2001 From: Nate Bradac Date: Thu, 23 Jul 2026 14:00:52 -0500 Subject: [PATCH] proposed cpp otf encoding logic --- sbe-tool/src/main/cpp/otf/Encoding.h | 152 +++++++ sbe-tool/src/main/cpp/otf/OtfHeaderEncoder.h | 116 ++++++ sbe-tool/src/main/cpp/otf/OtfMessageEncoder.h | 356 ++++++++++++++++ sbe-tool/src/test/cpp/CMakeLists.txt | 14 + .../test/cpp/otf/OtfMessageEncoderTest.cpp | 382 ++++++++++++++++++ 5 files changed, 1020 insertions(+) create mode 100644 sbe-tool/src/main/cpp/otf/OtfHeaderEncoder.h create mode 100644 sbe-tool/src/main/cpp/otf/OtfMessageEncoder.h create mode 100644 sbe-tool/src/test/cpp/otf/OtfMessageEncoderTest.cpp diff --git a/sbe-tool/src/main/cpp/otf/Encoding.h b/sbe-tool/src/main/cpp/otf/Encoding.h index 0f265117cc..5df626cb4b 100644 --- a/sbe-tool/src/main/cpp/otf/Encoding.h +++ b/sbe-tool/src/main/cpp/otf/Encoding.h @@ -501,6 +501,143 @@ class Encoding } } + static inline void putChar(char *buffer, char value) + { + *buffer = value; + } + + static inline void putInt8(char *buffer, std::int8_t value) + { + std::memcpy(buffer, &value, sizeof(std::int8_t)); + } + + static inline void putInt16(char *buffer, const ByteOrder byteOrder, std::int16_t value) + { + value = SBE_OTF_BYTE_ORDER_16(byteOrder, value); + std::memcpy(buffer, &value, sizeof(std::int16_t)); + } + + static inline void putInt32(char *buffer, const ByteOrder byteOrder, std::int32_t value) + { + value = SBE_OTF_BYTE_ORDER_32(byteOrder, value); + std::memcpy(buffer, &value, sizeof(std::int32_t)); + } + + static inline void putInt64(char *buffer, const ByteOrder byteOrder, std::int64_t value) + { + value = SBE_OTF_BYTE_ORDER_64(byteOrder, value); + std::memcpy(buffer, &value, sizeof(std::int64_t)); + } + + static inline void putUInt8(char *buffer, std::uint8_t value) + { + std::memcpy(buffer, &value, sizeof(std::uint8_t)); + } + + static inline void putUInt16(char *buffer, const ByteOrder byteOrder, std::uint16_t value) + { + value = SBE_OTF_BYTE_ORDER_16(byteOrder, value); + std::memcpy(buffer, &value, sizeof(std::uint16_t)); + } + + static inline void putUInt32(char *buffer, const ByteOrder byteOrder, std::uint32_t value) + { + value = SBE_OTF_BYTE_ORDER_32(byteOrder, value); + std::memcpy(buffer, &value, sizeof(std::uint32_t)); + } + + static inline void putUInt64(char *buffer, const ByteOrder byteOrder, std::uint64_t value) + { + value = SBE_OTF_BYTE_ORDER_64(byteOrder, value); + std::memcpy(buffer, &value, sizeof(std::uint64_t)); + } + + static inline void putFloat(char *buffer, const ByteOrder byteOrder, float value) + { + sbe_float_as_uint_t val; + val.fp_value = value; + val.uint_value = SBE_OTF_BYTE_ORDER_32(byteOrder, val.uint_value); + std::memcpy(buffer, &val, sizeof(float)); + } + + static inline void putDouble(char *buffer, const ByteOrder byteOrder, double value) + { + sbe_double_as_uint_t val; + val.fp_value = value; + val.uint_value = SBE_OTF_BYTE_ORDER_64(byteOrder, val.uint_value); + std::memcpy(buffer, &val, sizeof(double)); + } + + static inline void putInt(const PrimitiveType type, const ByteOrder byteOrder, char *buffer, const std::int64_t value) + { + switch (type) + { + case PrimitiveType::CHAR: + putChar(buffer, static_cast(value)); + break; + + case PrimitiveType::INT8: + putInt8(buffer, static_cast(value)); + break; + + case PrimitiveType::INT16: + putInt16(buffer, byteOrder, static_cast(value)); + break; + + case PrimitiveType::INT32: + putInt32(buffer, byteOrder, static_cast(value)); + break; + + case PrimitiveType::INT64: + putInt64(buffer, byteOrder, value); + break; + + default: + throw std::runtime_error("incorrect type for Encoding::putInt"); + } + } + + static inline void putUInt(const PrimitiveType type, const ByteOrder byteOrder, char *buffer, const std::uint64_t value) + { + switch (type) + { + case PrimitiveType::UINT8: + putUInt8(buffer, static_cast(value)); + break; + + case PrimitiveType::UINT16: + putUInt16(buffer, byteOrder, static_cast(value)); + break; + + case PrimitiveType::UINT32: + putUInt32(buffer, byteOrder, static_cast(value)); + break; + + case PrimitiveType::UINT64: + putUInt64(buffer, byteOrder, value); + break; + + default: + throw std::runtime_error("incorrect type for Encoding::putUInt"); + } + } + + static inline void putDouble(const PrimitiveType type, const ByteOrder byteOrder, char *buffer, const double value) + { + if (type == PrimitiveType::FLOAT) + { + putFloat(buffer, byteOrder, static_cast(value)); + } + else if (type == PrimitiveType::DOUBLE) + { + putDouble(buffer, byteOrder, value); + } + else + { + throw std::runtime_error("incorrect type for Encoding::putDouble"); + } + } + inline Presence presence() const { return m_presence; @@ -531,6 +668,21 @@ class Encoding return getDouble(m_primitiveType, m_byteOrder, buffer); } + inline void putAsInt(char *buffer, const std::int64_t value) const + { + putInt(m_primitiveType, m_byteOrder, buffer, value); + } + + inline void putAsUInt(char *buffer, const std::uint64_t value) const + { + putUInt(m_primitiveType, m_byteOrder, buffer, value); + } + + inline void putAsDouble(char *buffer, const double value) const + { + putDouble(m_primitiveType, m_byteOrder, buffer, value); + } + inline const PrimitiveValue &minValue() const { return m_minValue; diff --git a/sbe-tool/src/main/cpp/otf/OtfHeaderEncoder.h b/sbe-tool/src/main/cpp/otf/OtfHeaderEncoder.h new file mode 100644 index 0000000000..2d04fceff9 --- /dev/null +++ b/sbe-tool/src/main/cpp/otf/OtfHeaderEncoder.h @@ -0,0 +1,116 @@ +/* + * Copyright 2013-2025 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 _OTF_HEADERENCODER_H +#define _OTF_HEADERENCODER_H + +#include +#include +#include +#include + +#include "Token.h" + +namespace sbe { +namespace otf { + +/* + * Writes an SBE message header. Inverse of OtfHeaderDecoder: it locates the same well-known + * header fields by name and writes them, rather than reading them. + */ +class OtfHeaderEncoder +{ +public: + explicit OtfHeaderEncoder(const std::shared_ptr> &tokens) + { + m_encodedLength = tokens->at(0).encodedLength(); + + for (Token &token : *tokens) + { + const std::string &name = token.name(); + + if (name == "blockLength") { m_blockLength = field(token); } + else if (name == "templateId"){ m_templateId = field(token); } + else if (name == "schemaId") { m_schemaId = field(token); } + else if (name == "version") { m_schemaVersion = field(token); } + } + + require(m_blockLength, "blockLength"); + require(m_templateId, "templateId"); + require(m_schemaId, "schemaId"); + require(m_schemaVersion, "version"); + } + + inline std::uint32_t encodedLength() const + { + return static_cast(m_encodedLength); + } + + /* All header elements are unsigned integers per the SBE specification. */ + + void setBlockLength(char *headerBuffer, std::uint64_t value) const { put(m_blockLength, headerBuffer, value); } + void setTemplateId(char *headerBuffer, std::uint64_t value) const { put(m_templateId, headerBuffer, value); } + void setSchemaId(char *headerBuffer, std::uint64_t value) const { put(m_schemaId, headerBuffer, value); } + void setSchemaVersion(char *headerBuffer, std::uint64_t value) const { put(m_schemaVersion, headerBuffer, value); } + + void encode( + char *headerBuffer, + std::uint64_t blockLength, + std::uint64_t templateId, + std::uint64_t schemaId, + std::uint64_t schemaVersion) const + { + setBlockLength(headerBuffer, blockLength); + setTemplateId(headerBuffer, templateId); + setSchemaId(headerBuffer, schemaId); + setSchemaVersion(headerBuffer, schemaVersion); + } + +private: + struct Field + { + std::int32_t offset = -1; + PrimitiveType type = PrimitiveType::NONE; + ByteOrder byteOrder = ByteOrder::SBE_LITTLE_ENDIAN; + }; + + static Field field(const Token &token) + { + return { token.offset(), token.encoding().primitiveType(), token.encoding().byteOrder() }; + } + + static void require(const Field &f, const char *name) + { + if (f.offset < 0) + { + throw std::runtime_error(std::string(name) + " token not found"); + } + } + + static void put(const Field &f, char *headerBuffer, std::uint64_t value) + { + Encoding::putUInt(f.type, f.byteOrder, headerBuffer + f.offset, value); + } + + std::int32_t m_encodedLength = 0; + Field m_blockLength; + Field m_templateId; + Field m_schemaId; + Field m_schemaVersion; +}; + +}} + +#endif diff --git a/sbe-tool/src/main/cpp/otf/OtfMessageEncoder.h b/sbe-tool/src/main/cpp/otf/OtfMessageEncoder.h new file mode 100644 index 0000000000..18844442fe --- /dev/null +++ b/sbe-tool/src/main/cpp/otf/OtfMessageEncoder.h @@ -0,0 +1,356 @@ +/* + * Copyright 2013-2025 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 _OTF_MESSAGEENCODER_H +#define _OTF_MESSAGEENCODER_H + +#include +#include +#include +#include + +#include "Token.h" + +using namespace sbe::otf; + +namespace sbe { namespace otf { namespace OtfMessageEncoder { + +/* + * The on-the-fly encoder: the structural inverse of OtfMessageDecoder. It walks the same IR token + * stream and writes wire bytes, obtaining each field's value from a TokenEncoder. It owns all + * schema-driven mechanics (offsets, block/dimension framing, var-data length prefixes, + * constant-field skipping, byte order and acting-version gating). + * + * A TokenEncoder mirrors OtfMessageDecoder's TokenListener, with two differences: the char* + * buffers passed to its callbacks are destinations to be written (via Token::encoding().putAs*), + * not sources to read; and because repeating-group counts and variable-length-data lengths are + * not yet on the wire when encoding, the encoder queries them via numInGroup()/varDataLength(). + * See BasicTokenEncoder for the full callback surface. + */ + +class BasicTokenEncoder +{ +public: + virtual ~BasicTokenEncoder() = default; + + virtual void onBeginMessage(Token &) {} + + virtual void onEndMessage(Token &) {} + + virtual void onEncoding(Token &, char *, Token &, std::uint64_t) {} + + virtual void onEnum(Token &, char *, std::vector &, std::size_t, std::size_t, std::uint64_t) {} + + virtual void onBitSet(Token &, char *, std::vector &, std::size_t, std::size_t, std::uint64_t) {} + + virtual void onBeginComposite(Token &, std::vector &, std::size_t, std::size_t) {} + + virtual void onEndComposite(Token &, std::vector &, std::size_t, std::size_t) {} + + virtual std::uint64_t numInGroup(Token &) { return 0; } + + virtual void onBeginGroup(Token &, std::uint64_t, std::uint64_t) {} + + virtual void onEndGroup(Token &, std::uint64_t, std::uint64_t) {} + + virtual std::uint64_t varDataLength(Token &) { return 0; } + + virtual void onVarData(Token &, char *, std::uint64_t, Token &) {} +}; + +template +static void encodeComposite( + char *buffer, + std::size_t bufferIndex, + std::uint64_t actingVersion, + std::vector &tokens, + Token &fieldToken, + std::size_t tokenIndex, + std::size_t toIndex, + TokenEncoder &encoder) +{ + // Mirror of OtfMessageDecoder::decodeComposite: onBeginComposite/onEndComposite bracket the walk + // and carry the enclosing fieldToken, which is also threaded to enum/set members and nested + // composites (matching the decoder's callback contract). + encoder.onBeginComposite(fieldToken, tokens, tokenIndex, toIndex); + + for (std::size_t i = tokenIndex + 1; i < toIndex;) + { + Token &token = tokens.at(i); + const std::size_t nextIndex = i + token.componentTokenCount(); + const std::size_t offset = bufferIndex + token.offset(); + + switch (token.signal()) + { + case Signal::BEGIN_COMPOSITE: + encodeComposite(buffer, offset, actingVersion, tokens, fieldToken, i, nextIndex - 1, encoder); + break; + + case Signal::BEGIN_ENUM: + if (!token.isConstantEncoding()) + { + encoder.onEnum(fieldToken, buffer + offset, tokens, i, nextIndex - 1, actingVersion); + } + break; + + case Signal::BEGIN_SET: + if (!token.isConstantEncoding()) + { + encoder.onBitSet(fieldToken, buffer + offset, tokens, i, nextIndex - 1, actingVersion); + } + break; + + case Signal::ENCODING: + // constant-presence fields carry no wire bytes; their value lives in the schema + if (!token.isConstantEncoding()) + { + encoder.onEncoding(token, buffer + offset, token, actingVersion); + } + break; + + default: + break; + } + + i += token.componentTokenCount(); + } + + encoder.onEndComposite(fieldToken, tokens, tokenIndex, toIndex); +} + +template +static std::size_t encodeFields( + char *buffer, + std::size_t bufferIndex, + std::uint64_t actingVersion, + std::vector &tokens, + std::size_t tokenIndex, + const std::size_t numTokens, + TokenEncoder &encoder) +{ + while (tokenIndex < numTokens) + { + Token &fieldToken = tokens.at(tokenIndex); + if (Signal::BEGIN_FIELD != fieldToken.signal()) + { + break; + } + + const std::size_t nextFieldIndex = tokenIndex + fieldToken.componentTokenCount(); + tokenIndex++; + + Token &typeToken = tokens.at(tokenIndex); + const std::size_t offset = bufferIndex + typeToken.offset(); + + switch (typeToken.signal()) + { + case Signal::BEGIN_COMPOSITE: + encodeComposite(buffer, offset, actingVersion, tokens, fieldToken, tokenIndex, nextFieldIndex - 2, encoder); + break; + + case Signal::BEGIN_ENUM: + if (!fieldToken.isConstantEncoding()) + { + encoder.onEnum(fieldToken, buffer + offset, tokens, tokenIndex, nextFieldIndex - 2, actingVersion); + } + break; + + case Signal::BEGIN_SET: + if (!fieldToken.isConstantEncoding()) + { + encoder.onBitSet(fieldToken, buffer + offset, tokens, tokenIndex, nextFieldIndex - 2, actingVersion); + } + break; + + case Signal::ENCODING: + if (!typeToken.isConstantEncoding()) + { + encoder.onEncoding(fieldToken, buffer + offset, typeToken, actingVersion); + } + break; + + default: + break; + } + + tokenIndex = nextFieldIndex; + } + + return tokenIndex; +} + +template +static std::size_t encodeData( + char *buffer, + std::size_t bufferIndex, + const std::size_t length, + std::vector &tokens, + std::size_t tokenIndex, + const std::size_t numTokens, + std::uint64_t actingVersion, + TokenEncoder &encoder) +{ + while (tokenIndex < numTokens) + { + Token &token = tokens.at(tokenIndex); + if (Signal::BEGIN_VAR_DATA != token.signal()) + { + break; + } + + const bool isPresent = token.tokenVersion() <= static_cast(actingVersion); + + if (isPresent) + { + Token &lengthToken = tokens.at(tokenIndex + 2); + Token &dataToken = tokens.at(tokenIndex + 3); + + if ((bufferIndex + dataToken.offset()) > length) + { + throw std::runtime_error("length too short for data length field"); + } + + const std::uint64_t dataLength = encoder.varDataLength(token); + lengthToken.encoding().putAsUInt(buffer + bufferIndex + lengthToken.offset(), dataLength); + + bufferIndex += dataToken.offset(); + + if ((bufferIndex + dataLength) > length) + { + throw std::runtime_error("length too short for data field"); + } + + encoder.onVarData(token, buffer + bufferIndex, dataLength, dataToken); + bufferIndex += dataLength; + } + + tokenIndex += token.componentTokenCount(); + } + + return bufferIndex; +} + +template +static std::pair encodeGroups( + char *buffer, + std::size_t bufferIndex, + const std::size_t length, + std::uint64_t actingVersion, + std::vector &tokens, + std::size_t tokenIndex, + const std::size_t numTokens, + TokenEncoder &encoder) +{ + while (tokenIndex < numTokens) + { + Token &token = tokens.at(tokenIndex); + if (Signal::BEGIN_GROUP != token.signal()) + { + break; + } + + const bool isPresent = token.tokenVersion() <= static_cast(actingVersion); + + Token &dimensionsTypeComposite = tokens.at(tokenIndex + 1); + Token &blockLengthToken = tokens.at(tokenIndex + 2); + Token &numInGroupToken = tokens.at(tokenIndex + 3); + + auto dimensionsLength = static_cast(dimensionsTypeComposite.encodedLength()); + // a BEGIN_GROUP token's encodedLength is the group's per-entry block length + auto blockLength = static_cast(token.encodedLength()); + const std::uint64_t numInGroup = isPresent ? encoder.numInGroup(token) : 0; + + if (isPresent) + { + if ((bufferIndex + dimensionsLength) > length) + { + throw std::runtime_error("length too short for group dimensions"); + } + + blockLengthToken.encoding().putAsUInt(buffer + bufferIndex + blockLengthToken.offset(), blockLength); + numInGroupToken.encoding().putAsUInt(buffer + bufferIndex + numInGroupToken.offset(), numInGroup); + + bufferIndex += dimensionsLength; + } + + size_t beginFieldsIndex = tokenIndex + dimensionsTypeComposite.componentTokenCount() + 1; + + for (std::uint64_t i = 0; i < numInGroup; i++) + { + encoder.onBeginGroup(token, i, numInGroup); + + if ((bufferIndex + blockLength) > length) + { + throw std::runtime_error("length too short for group blockLength"); + } + + size_t afterFieldsIndex = encodeFields( + buffer, bufferIndex, actingVersion, tokens, beginFieldsIndex, numTokens, encoder); + bufferIndex += blockLength; + + std::pair groupsResult = encodeGroups( + buffer, bufferIndex, length, actingVersion, tokens, afterFieldsIndex, numTokens, encoder); + + bufferIndex = encodeData( + buffer, groupsResult.first, length, tokens, groupsResult.second, numTokens, actingVersion, encoder); + + encoder.onEndGroup(token, i, numInGroup); + } + + tokenIndex += token.componentTokenCount(); + } + + return { bufferIndex, tokenIndex }; +} + +/** + * Entry point for the encoder. `buffer` points past the message header; `blockLength` is the + * message's root block length. Returns the number of bytes written (block + groups + var-data). + */ +template +std::size_t encode( + char *buffer, + const std::size_t length, + std::uint64_t actingVersion, + std::size_t blockLength, + std::shared_ptr> msgTokens, + TokenEncoder &encoder) +{ + encoder.onBeginMessage(msgTokens->at(0)); + + if (length < blockLength) + { + throw std::runtime_error("length too short for message blockLength"); + } + + size_t numTokens = msgTokens->size(); + const size_t tokenIndex = encodeFields(buffer, 0, actingVersion, *msgTokens, 1, numTokens, encoder); + + size_t bufferIndex = blockLength; + + std::pair groupResult = encodeGroups( + buffer, bufferIndex, length, actingVersion, *msgTokens, tokenIndex, numTokens, encoder); + + bufferIndex = encodeData( + buffer, groupResult.first, length, *msgTokens, groupResult.second, numTokens, actingVersion, encoder); + + encoder.onEndMessage(msgTokens->at(numTokens - 1)); + + return bufferIndex; +} + +}}} + +#endif diff --git a/sbe-tool/src/test/cpp/CMakeLists.txt b/sbe-tool/src/test/cpp/CMakeLists.txt index 0137b6de35..dfa1e4c5a6 100644 --- a/sbe-tool/src/test/cpp/CMakeLists.txt +++ b/sbe-tool/src/test/cpp/CMakeLists.txt @@ -103,6 +103,20 @@ sbe_test(FieldAccessOrderCheckTest codecs) target_compile_definitions(FieldAccessOrderCheckTest PRIVATE SBE_ENABLE_PRECEDENCE_CHECKS_IN_TESTS) sbe_test(VersionedMessageTest codecs) +# OtfMessageEncoderTest lives in the otf/ subdirectory and provides its own main() that takes the +# IR file path as argv[1]; it is handed the generated code-generation-schema IR. +add_executable(OtfMessageEncoderTest otf/OtfMessageEncoderTest.cpp) +add_dependencies(OtfMessageEncoderTest gmock codecs) +target_include_directories(OtfMessageEncoderTest PRIVATE ${CXX_CODEC_TARGET_DIR}) +target_compile_options(OtfMessageEncoderTest PRIVATE $<$:-Werror>) +target_compile_options(OtfMessageEncoderTest PRIVATE $<$:-Werror>) +target_compile_options(OtfMessageEncoderTest PRIVATE $<$:-Werror>) +target_link_libraries(OtfMessageEncoderTest sbe gmock_main ${CMAKE_THREAD_LIBS_INIT}) +add_test( + NAME OtfMessageEncoderTest + COMMAND OtfMessageEncoderTest ${CXX_CODEC_TARGET_DIR}/code-generation-schema.sbeir + WORKING_DIRECTORY ${CXX_CODEC_TARGET_DIR}) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # Check if the GCC version supports C++17 if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "7.0") diff --git a/sbe-tool/src/test/cpp/otf/OtfMessageEncoderTest.cpp b/sbe-tool/src/test/cpp/otf/OtfMessageEncoderTest.cpp new file mode 100644 index 0000000000..5b0bae5724 --- /dev/null +++ b/sbe-tool/src/test/cpp/otf/OtfMessageEncoderTest.cpp @@ -0,0 +1,382 @@ +/* + * Copyright 2013-2025 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 +#include +#include + +#include "gtest/gtest.h" + +#include "otf/Token.h" +#include "otf/Encoding.h" +#include "otf/IrDecoder.h" +#include "otf/OtfHeaderDecoder.h" +#include "otf/OtfHeaderEncoder.h" +#include "otf/OtfMessageDecoder.h" +#include "otf/OtfMessageEncoder.h" + +#include "code_generation_test/Car.h" + +using namespace sbe::otf; +using namespace code::generation::test; + +// IR (.sbeir) for the example schema, produced by SbeTool; supplied on argv or $SBE_EXAMPLE_IR. +static std::string g_irFile; + +// ================================================================================================= +// Encoding::put* / putAs* <-> get* / getAs* +// ================================================================================================= +class EncodingPutTest : public ::testing::TestWithParam {}; + +TEST_P(EncodingPutTest, IntRoundTrip) +{ + const ByteOrder bo = GetParam(); + char b[8] = {}; + + for (const std::int64_t v : { std::int64_t(0), std::int64_t(-1), std::int64_t(1), + std::int64_t(-128), std::int64_t(127) }) + { + Encoding::putInt(PrimitiveType::INT8, bo, b, v); + EXPECT_EQ(Encoding::getInt(PrimitiveType::INT8, bo, b), v); + } + Encoding::putInt(PrimitiveType::INT16, bo, b, -12345); + EXPECT_EQ(Encoding::getInt(PrimitiveType::INT16, bo, b), -12345); + Encoding::putInt(PrimitiveType::INT32, bo, b, -1234567); + EXPECT_EQ(Encoding::getInt(PrimitiveType::INT32, bo, b), -1234567); + Encoding::putInt(PrimitiveType::INT64, bo, b, -1234567890123LL); + EXPECT_EQ(Encoding::getInt(PrimitiveType::INT64, bo, b), -1234567890123LL); +} + +TEST_P(EncodingPutTest, UIntRoundTrip) +{ + const ByteOrder bo = GetParam(); + char b[8] = {}; + + Encoding::putUInt(PrimitiveType::UINT8, bo, b, 200u); + EXPECT_EQ(Encoding::getUInt(PrimitiveType::UINT8, bo, b), 200u); + Encoding::putUInt(PrimitiveType::UINT16, bo, b, 54321u); + EXPECT_EQ(Encoding::getUInt(PrimitiveType::UINT16, bo, b), 54321u); + Encoding::putUInt(PrimitiveType::UINT32, bo, b, 4000000000u); + EXPECT_EQ(Encoding::getUInt(PrimitiveType::UINT32, bo, b), 4000000000u); + Encoding::putUInt(PrimitiveType::UINT64, bo, b, 0xdeadbeefcafef00dULL); + EXPECT_EQ(Encoding::getUInt(PrimitiveType::UINT64, bo, b), 0xdeadbeefcafef00dULL); +} + +TEST_P(EncodingPutTest, FloatingPointRoundTrip) +{ + const ByteOrder bo = GetParam(); + char b[8] = {}; + + Encoding::putDouble(PrimitiveType::FLOAT, bo, b, 3.5f); + EXPECT_EQ(Encoding::getDouble(PrimitiveType::FLOAT, bo, b), 3.5); + Encoding::putDouble(PrimitiveType::DOUBLE, bo, b, 2.718281828459045); + EXPECT_EQ(Encoding::getDouble(PrimitiveType::DOUBLE, bo, b), 2.718281828459045); +} + +INSTANTIATE_TEST_SUITE_P( + BothByteOrders, EncodingPutTest, + ::testing::Values(ByteOrder::SBE_LITTLE_ENDIAN, ByteOrder::SBE_BIG_ENDIAN)); + +TEST(EncodingPutTest, PutThrowsOnWrongType) +{ + char b[8] = {}; + EXPECT_THROW(Encoding::putInt(PrimitiveType::UINT8, ByteOrder::SBE_LITTLE_ENDIAN, b, 1), std::runtime_error); + EXPECT_THROW(Encoding::putUInt(PrimitiveType::INT8, ByteOrder::SBE_LITTLE_ENDIAN, b, 1), std::runtime_error); + EXPECT_THROW(Encoding::putDouble(PrimitiveType::INT8, ByteOrder::SBE_LITTLE_ENDIAN, b, 1.0), std::runtime_error); +} + +// ================================================================================================= +// A raw-byte DOM: a decode records each leaf field's exact wire bytes, its groups and composites; +// an encode replays them. If the encoder walks the token stream and frames the message correctly, +// the re-encoded bytes are identical to the original. This exercises the full engine +// (fields / composites / groups / nested groups / var-data), OtfHeaderEncoder, and the +// dimension/length writes, and is a strict regression guard: a field recorded under one path but +// looked up under another (as an incorrect composite/enum callback would cause) aborts the replay. +// ================================================================================================= +namespace { + +struct Node +{ + std::map leaves; + std::map composites; + std::map> groups; +}; + +std::size_t enumOrSetLength(std::vector &tokens, std::size_t fromIndex) +{ + return lengthOfType(tokens.at(fromIndex + 1).encoding().primitiveType()); +} + +struct RecordingListener : public OtfMessageDecoder::BasicTokenListener +{ + Node root; + std::vector stack{ &root }; + Node &cur() { return *stack.back(); } + + void onEncoding(Token &f, const char *buffer, Token &type, std::uint64_t) override + { + cur().leaves[f.name()] = std::string(buffer, type.encodedLength()); + } + void onEnum(Token &f, const char *buffer, std::vector &t, std::size_t from, std::size_t, std::uint64_t) override + { + cur().leaves[f.name()] = std::string(buffer, enumOrSetLength(t, from)); + } + void onBitSet(Token &f, const char *buffer, std::vector &t, std::size_t from, std::size_t, std::uint64_t) override + { + cur().leaves[f.name()] = std::string(buffer, enumOrSetLength(t, from)); + } + void onBeginComposite(Token &f, std::vector &, std::size_t, std::size_t) override + { + stack.push_back(&cur().composites[f.name()]); + } + void onEndComposite(Token &, std::vector &, std::size_t, std::size_t) override { stack.pop_back(); } + void onBeginGroup(Token &token, std::uint64_t, std::uint64_t) override + { + auto &entries = cur().groups[token.name()]; + entries.emplace_back(); + stack.push_back(&entries.back()); + } + void onEndGroup(Token &, std::uint64_t, std::uint64_t) override { stack.pop_back(); } + void onVarData(Token &f, const char *buffer, std::uint64_t length, Token &) override + { + cur().leaves[f.name()] = std::string(buffer, length); + } +}; + +template +const typename M::mapped_type &require(const M &m, const std::string &k) +{ + auto it = m.find(k); + EXPECT_NE(it, m.end()) << "replay could not find '" << k << "' recorded on decode"; + return it->second; // throws std::out_of_range if truly absent, failing the test cleanly +} + +struct ReplayEncoder : public OtfMessageEncoder::BasicTokenEncoder +{ + const Node &root; + std::vector stack; + explicit ReplayEncoder(const Node &r) : root(r) { stack.push_back(&root); } + const Node &cur() const { return *stack.back(); } + + void onEncoding(Token &f, char *buffer, Token &, std::uint64_t) override + { + const std::string &raw = require(cur().leaves, f.name()); + std::memcpy(buffer, raw.data(), raw.size()); + } + void onEnum(Token &f, char *buffer, std::vector &, std::size_t, std::size_t, std::uint64_t) override + { + const std::string &raw = require(cur().leaves, f.name()); + std::memcpy(buffer, raw.data(), raw.size()); + } + void onBitSet(Token &f, char *buffer, std::vector &, std::size_t, std::size_t, std::uint64_t) override + { + const std::string &raw = require(cur().leaves, f.name()); + std::memcpy(buffer, raw.data(), raw.size()); + } + void onBeginComposite(Token &f, std::vector &, std::size_t, std::size_t) override + { + stack.push_back(&require(cur().composites, f.name())); + } + void onEndComposite(Token &, std::vector &, std::size_t, std::size_t) override { stack.pop_back(); } + std::uint64_t numInGroup(Token &token) override + { + auto it = cur().groups.find(token.name()); + return it == cur().groups.end() ? 0 : it->second.size(); + } + void onBeginGroup(Token &token, std::uint64_t index, std::uint64_t) override + { + stack.push_back(&require(cur().groups, token.name())[index]); + } + void onEndGroup(Token &, std::uint64_t, std::uint64_t) override { stack.pop_back(); } + std::uint64_t varDataLength(Token &f) override + { + auto it = cur().leaves.find(f.name()); + return it == cur().leaves.end() ? 0 : it->second.size(); + } + void onVarData(Token &f, char *buffer, std::uint64_t length, Token &) override + { + const std::string &raw = require(cur().leaves, f.name()); + std::memcpy(buffer, raw.data(), length); + } +}; + +// decode `reference` (a message written into a `capacity`-byte buffer) then re-encode it; assert +// the result is byte-identical. The true framed length comes from the decode, not the caller. +void assertOtfRoundTripIsIdentical(const char *reference, std::size_t capacity) +{ + IrDecoder ir; + ASSERT_EQ(ir.decode(g_irFile.c_str()), 0) << "failed to load IR: " << g_irFile; + auto headerTokens = ir.header(); + + OtfHeaderDecoder headerDecoder(headerTokens); + const std::size_t headerLength = headerDecoder.encodedLength(); + const std::uint64_t blockLength = headerDecoder.getBlockLength(reference); + const std::uint64_t templateId = headerDecoder.getTemplateId(reference); + const std::uint64_t schemaId = headerDecoder.getSchemaId(reference); + const std::uint64_t version = headerDecoder.getSchemaVersion(reference); + + auto msgTokens = ir.message(static_cast(templateId), static_cast(version)); + ASSERT_TRUE(msgTokens); + + RecordingListener rec; + const std::size_t decodedBody = OtfMessageDecoder::decode( + reference + headerLength, capacity - headerLength, version, blockLength, msgTokens, rec); + const std::size_t refLen = headerLength + decodedBody; + + std::vector out(capacity + 64, '\0'); + + OtfHeaderEncoder headerEncoder(headerTokens); + EXPECT_EQ(headerEncoder.encodedLength(), headerLength); + headerEncoder.encode(out.data(), blockLength, templateId, schemaId, version); + + ReplayEncoder rep(rec.root); + const std::size_t encodedBody = OtfMessageEncoder::encode( + out.data() + headerLength, out.size() - headerLength, version, blockLength, msgTokens, rep); + + ASSERT_EQ(headerLength + encodedBody, refLen); + EXPECT_EQ(0, std::memcmp(out.data(), reference, refLen)); +} + +// Build a reference Car via the generated (jar-produced) flyweight — an independent encoder. +std::size_t encodeCar(char *buffer, std::size_t capacity, + std::uint16_t fuelFigures, std::uint16_t performanceFigures, std::uint16_t accelerationPer, + const std::string &manufacturer, const std::string &model, const std::string &activationCode) +{ + std::memset(buffer, 0, capacity); + + Car car; + car.wrapAndApplyHeader(buffer, 0, static_cast(capacity)); + car.serialNumber(1234567ULL); + car.modelYear(2024); + car.available(BooleanType::T); + car.code(Model::C); + for (std::uint64_t i = 0; i < Car::someNumbersLength(); i++) + { + car.someNumbers(i, static_cast(10 * (i + 1))); + } + + Car::FuelFigures &ff = car.fuelFiguresCount(fuelFigures); + for (std::uint16_t i = 0; i < fuelFigures; i++) + { + ff.next().speed(static_cast(30 + 30 * i)).mpg(35.5f - i) + .putUsageDescription(std::string("usage-") + std::to_string(i)); // group-level var-data + } + + Car::PerformanceFigures &pf = car.performanceFiguresCount(performanceFigures); + for (std::uint16_t i = 0; i < performanceFigures; i++) + { + Car::PerformanceFigures::Acceleration &acc = + pf.next().octaneRating(static_cast(95 + i)).accelerationCount(accelerationPer); + for (std::uint16_t j = 0; j < accelerationPer; j++) + { + acc.next().mph(static_cast(30 * (j + 1))).seconds(4.5f + j); + } + } + + car.putManufacturer(manufacturer); + car.putModel(model); + car.putActivationCode(activationCode); + + return Car::messageHeader::encodedLength() + car.encodedLength(); +} + +} // namespace + +// ================================================================================================= +// Message round-trips (each is byte-identical decode -> encode) +// ================================================================================================= +class OtfMessageEncoderTest : public ::testing::Test +{ +protected: + void SetUp() override + { + if (g_irFile.empty()) + { + GTEST_SKIP() << "set $SBE_EXAMPLE_IR (or pass the .sbeir path on argv) to run round-trip tests"; + } + } + char m_buffer[8192]; +}; + +TEST_F(OtfMessageEncoderTest, FullyPopulatedCar) +{ + (void) encodeCar(m_buffer, sizeof(m_buffer), 2, 2, 2, "Honda", "Civic", "abcdef"); + assertOtfRoundTripIsIdentical(m_buffer, sizeof(m_buffer)); +} + +TEST_F(OtfMessageEncoderTest, EmptyGroupsAndVarData) +{ + (void) encodeCar(m_buffer, sizeof(m_buffer), 0, 0, 0, "", "", ""); + assertOtfRoundTripIsIdentical(m_buffer, sizeof(m_buffer)); +} + +TEST_F(OtfMessageEncoderTest, EmptyNestedGroup) +{ + // performance figures present, but each with zero acceleration entries + (void) encodeCar(m_buffer, sizeof(m_buffer), 1, 3, 0, "M", "Md", "code"); + assertOtfRoundTripIsIdentical(m_buffer, sizeof(m_buffer)); +} + +TEST_F(OtfMessageEncoderTest, ManyGroupEntriesAndNestedGroups) +{ + (void) encodeCar(m_buffer, sizeof(m_buffer), 5, 4, 3, + "AVeryLongManufacturerName", "AVeryLongModelName", "activation-code-value"); + assertOtfRoundTripIsIdentical(m_buffer, sizeof(m_buffer)); +} + +// ================================================================================================= +// OtfHeaderEncoder writes a header that OtfHeaderDecoder reads back unchanged +// ================================================================================================= +TEST(OtfHeaderEncoderTest, RoundTripsHeaderFields) +{ + if (g_irFile.empty()) + { + GTEST_SKIP() << "set $SBE_EXAMPLE_IR to run this test"; + } + + IrDecoder ir; + ASSERT_EQ(ir.decode(g_irFile.c_str()), 0); + auto headerTokens = ir.header(); + + OtfHeaderEncoder encoder(headerTokens); + OtfHeaderDecoder decoder(headerTokens); + + char header[64] = {}; + encoder.encode(header, 47, 1, 10000, 1); + + EXPECT_EQ(decoder.getBlockLength(header), 47u); + EXPECT_EQ(decoder.getTemplateId(header), 1u); + EXPECT_EQ(decoder.getSchemaId(header), 10000u); + EXPECT_EQ(decoder.getSchemaVersion(header), 1u); + EXPECT_EQ(encoder.encodedLength(), decoder.encodedLength()); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + if (const char *env = std::getenv("SBE_EXAMPLE_IR")) + { + g_irFile = env; + } + if (argc > 1) + { + g_irFile = argv[1]; + } + return RUN_ALL_TESTS(); +}