Files
fsfw/unittests/globalfunctions/testCobsEncoder.cpp
T
2026-08-15 12:02:57 +02:00

337 lines
14 KiB
C++

#include <algorithm>
#include <array>
#include <cstdint>
#include <vector>
#include "catch2/catch_test_macros.hpp"
#include "fsfw/globalfunctions/CobsEncoder.h"
#include "fsfw/returnvalues/returnvalue.h"
// no zero bytes
constexpr std::array<uint8_t, 3> ti0 = {1, 2, 43};
constexpr std::array<uint8_t, 5> to0 = {0x04, 0x01, 0x02, 0x2b, 0x00};
// single zero byte
constexpr std::array<uint8_t, 4> ti1 = {1, 2, 0, 3};
constexpr std::array<uint8_t, 6> to1 = {3, 1, 2, 2, 3, 0};
// multiple zero bytes in sequence
constexpr std::array<uint8_t, 6> ti2 = {1, 2, 0, 0, 0, 3};
constexpr std::array<uint8_t, 8> to2 = {3, 1, 2, 1, 1, 2, 3, 0};
// stuffing bytes required because of the length without a single zero byte
// clang-format off
constexpr std::array<uint8_t, 257> ti3 = {
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
// #257
0xaa,
};
constexpr std::array<uint8_t, 260> to3 = {
0xff,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4,5 , 6, 7, 8, 9, 10, 11, 12, 13, 14,
// #255 -> stuffing
0x04,
15, 16,
0xaa, 0
};
// clang-format on
// real packet data
// PLOC startup packets (usually in one or two messages, but 3 separate COBS encoded packets)
// clang-format off
constexpr std::array<uint8_t, 36> ploc_startup_1_i = {
0x08,0x42,0xc0,0x00,0x00,0x1d,0x20,0x05,0x02,0x00,0x00,0x00,0x00,0x40,0x61,0x0d,0x02,0x8e,
0xb4,0xef,0x2e,0xe1,0x44,0x00,0x1f,0x03,0x00,0x00,0x52,0x18,0x00,0x00,0x00,0x00,0x1b,0xe2,
};
constexpr std::array<uint8_t, 36> ploc_startup_2_i = {
0x08,0x42,0xc0,0x01,0x00,0x1d,0x20,0x05,0x02,0x00,0x01,0x00,0x00,0x40,0x61,0x0d,0x02,0x8e,
0xb4,0xef,0x30,0x74,0x44,0x00,0x1f,0x07,0x00,0x00,0x50,0x03,0x00,0x00,0x00,0x00,0x98,0xe3,
};
constexpr std::array<uint8_t, 36> ploc_startup_3_i = {
0x08,0x42,0xc0,0x02,0x00,0x1d,0x20,0x05,0x01,0x00,0x02,0x00,0x00,0x40,0x61,0x0d,0x02,0x8e,
0xb5,0xb8,0x30,0x73,0x44,0x00,0x1f,0x07,0x00,0x00,0x3b,0x04,0x00,0x00,0x00,0x00,0x22,0x68,
};
constexpr std::array<uint8_t, 38> ploc_startup_1_o = {
0x04,0x08,0x42,0xc0,0x01,0x05,0x1d,0x20,0x05,0x02,
0x01,0x01,0x01,0x0b,0x40,0x61,0x0d,0x02,0x8e,0xb4,
0xef,0x2e,0xe1,0x44,0x03,0x1f,0x03,0x01,0x03,0x52,
0x18,0x01,0x01,0x01,0x03,0x1b,0xe2, 0x00,
};
constexpr std::array<uint8_t, 38> ploc_startup_2_o = {
0x05, 0x08,
0x42,0xc0,0x01,0x05,0x1d,0x20,0x05,0x02,0x02,0x01,
0x01,0x0b,0x40,0x61,0x0d,0x02,0x8e,0xb4,0xef,0x30,
0x74,0x44,0x03,0x1f,0x07,0x01,0x03,0x50,0x03,0x01,
0x01,0x01,0x03,0x98,0xe3,0x00
};
constexpr std::array<uint8_t, 38> ploc_startup_3_o = {
0x05,0x08,0x42,0xc0,0x02,0x05,0x1d,0x20,0x05,0x01,
0x02,0x02,0x01,0x0b,0x40,0x61,0x0d,0x02,0x8e,0xb5,
0xb8,0x30,0x73,0x44,0x03,0x1f,0x07,0x01,0x03,0x3b,
0x04,0x01,0x01,0x01,0x03,0x22,0x68,0x00,
};
// clang-format on
// PLOC Ping (sent from OBC)
// clang-format off
constexpr std::array<uint8_t, 13> ploc_ping_i = {
// as received by PLOC
0x18,0x42,0xc0,0x00,0x00,0x06,0x2f,0x11,0x01,0x00,
0x00,0xd5,0xc8,
};
constexpr std::array<uint8_t, 15> ploc_ping_o = {
// as sent from OBC
0x04,0x18,0x42,0xc0,0x01,0x05,0x06,0x2f,0x11,0x01,0x01,0x03,0xd5,0xc8,0x00,
};
// clang-format on
// Corresponding PLOC Pong (sent from PLOC)
// clang-format off
constexpr std::array<uint8_t, 26> ploc_pong_i = {
// as received by OBC
0x08,0x42,0xc0,0x05,0x00,0x13,0x20,0x01,0x07,0x00,
0x01,0x00,0x00,0x40,0x61,0x0d,0x02,0x98,0xbd,0xab,
0x18,0x42,0xc0,0x00,0x13,0xd5,
};
constexpr std::array<uint8_t, 28> ploc_pong_o = {
// as sent by PLOC
0x05,0x08,0x42,0xc0,0x05,0x05,0x13,0x20,0x01,0x07,
0x02,0x01,0x01,0x0b,0x40,0x61,0x0d,0x02,0x98,0xbd,
0xab,0x18,0x42,0xc0,0x03,0x13,0xd5,0x00,
};
// clang-format on
#define FOR_EACH_TEST_ARRAY(macro) \
macro(ti0, to0); \
macro(ti1, to1); \
macro(ti2, to2); \
macro(ti3, to3); \
macro(ploc_startup_1_i, ploc_startup_1_o); \
macro(ploc_startup_2_i, ploc_startup_2_o); \
macro(ploc_startup_3_i, ploc_startup_3_o); \
macro(ploc_ping_i, ploc_ping_o); \
macro(ploc_pong_i, ploc_pong_o);
template <typename FI, typename FE, typename SI>
void check_iter_equal(FI first_begin, FE first_end, SI second_begin) {
auto first_iter = first_begin;
auto second_iter = second_begin;
auto i = 0;
while (first_iter < first_end) {
CHECK(*first_iter == *second_iter);
++first_iter;
++second_iter;
++i;
}
// safety check
CHECK(std::equal(first_begin, first_end, second_begin));
}
#define ENCODE_TEST(input, output) \
TEST_CASE("COBS encode " #input, "[cobs]") { \
std::vector<uint8_t> outstream; \
outstream.resize(output.size()); \
size_t encodedLen = 0; \
auto result = CobsEncoder::encode(input.data(), input.size(), outstream.data(), \
outstream.size(), &encodedLen); \
if (result == returnvalue::OK) { \
CHECK(encodedLen == output.size()); \
check_iter_equal(outstream.begin(), outstream.end(), output.begin()); \
} else { \
CHECK(result == returnvalue::OK); \
} \
}
FOR_EACH_TEST_ARRAY(ENCODE_TEST)
#undef ENCODE_TEST
#define DECODE_TEST(input, output) \
TEST_CASE("COBS decode " #output, "[cobs]") { \
std::vector<uint8_t> instream; \
instream.resize(input.size()); \
size_t decodedLen = 0; \
size_t readLen = 0; \
auto result = CobsEncoder::decode(output.data(), output.size(), &readLen, instream.data(), \
instream.size(), &decodedLen); \
if (result == returnvalue::OK) { \
CHECK(readLen == output.size()); \
CHECK(decodedLen == input.size()); \
check_iter_equal(instream.begin(), instream.end(), input.begin()); \
} else { \
CHECK(result == returnvalue::OK); \
} \
}
FOR_EACH_TEST_ARRAY(DECODE_TEST)
#undef DECODE_TEST
// payloads far past any block count round-trip and the worst case bound is tight enough to hold
// them
TEST_CASE("Large payload", "[cobs]") {
// no zero bytes at all, so this needs the maximum number of blocks the input length allows
std::vector<uint8_t> input(64 * 1024, 1);
std::vector<uint8_t> encoded(CobsEncoder::worstCaseEncodedLen(input.size()));
size_t encodedLen = 0;
auto result =
CobsEncoder::encode(input.data(), input.size(), encoded.data(), encoded.size(), &encodedLen);
REQUIRE(result == returnvalue::OK);
CHECK(encodedLen == encoded.size());
std::vector<uint8_t> decoded(input.size());
size_t decodedLen = 0;
size_t readLen = 0;
result = CobsEncoder::decode(encoded.data(), encodedLen, &readLen, decoded.data(), decoded.size(),
&decodedLen);
REQUIRE(result == returnvalue::OK);
CHECK(readLen == encodedLen);
REQUIRE(decodedLen == input.size());
CHECK(std::equal(decoded.begin(), decoded.end(), input.begin()));
}
// check that too small target buffers are not blindly used
TEST_CASE("Insufficient space", "[cobs]") {
std::vector<uint8_t> output{0, 2};
size_t encodedLen = 0;
auto result =
CobsEncoder::encode(ti1.data(), ti1.size(), output.data(), output.size(), &encodedLen);
CHECK(result == CobsEncoder::INSUFFICIENT_SPACE);
// even equal size should not work
output.resize(ti1.size());
result = CobsEncoder::encode(ti1.data(), ti1.size(), output.data(), output.size(), &encodedLen);
CHECK(result == CobsEncoder::INSUFFICIENT_SPACE);
// check reverse direction for decoding
output.resize(2);
size_t readLen;
size_t decodedLen;
result = CobsEncoder::decode(to1.data(), to1.size(), &readLen, output.data(), output.size(),
&decodedLen);
CHECK(result == CobsEncoder::INSUFFICIENT_SPACE);
}
TEST_CASE("Malformed COBS data", "[cobs]") {
// decode a cut-off packet
std::vector<uint8_t> input{std::begin(to2), std::end(to2)};
input.resize(to2.size() / 2);
std::vector<uint8_t> output;
output.resize(ti2.size() * 2);
size_t encodedLen;
size_t readLen;
auto result = CobsEncoder::decode(input.data(), input.size(), &readLen, output.data(),
output.size(), &encodedLen);
CHECK(result == CobsEncoder::STREAM_TOO_SHORT);
// inject a zero byte somewhere in the middle of a list of nonzero bytes
input = {10, 1, 2, 3, 4, 0, 6, 7, 8, 9, 0};
result = CobsEncoder::decode(input.data(), input.size(), &readLen, output.data(), output.size(),
&encodedLen);
CHECK(result == CobsEncoder::DECODING_ERROR);
// the whole broken frame is skipped, up to and including its delimiter
CHECK(readLen == 6);
}
// a corrupt frame must not hold back the intact frames queued behind it
TEST_CASE("Resynchronisation after a corrupt frame", "[cobs]") {
// a block claiming far more bytes than the frame holds, terminated like a real frame
std::vector<uint8_t> stream{0x40, 0x11, 0x22, 0x00};
const size_t corruptLen = stream.size();
stream.insert(stream.end(), to1.begin(), to1.end());
std::vector<uint8_t> output(ti1.size());
size_t readLen = 0;
size_t decodedLen = 0;
auto result = CobsEncoder::decode(stream.data(), stream.size(), &readLen, output.data(),
output.size(), &decodedLen);
REQUIRE(result == CobsEncoder::DECODING_ERROR);
REQUIRE(readLen == corruptLen);
// the next frame decodes from where the corrupt one ended
result = CobsEncoder::decode(stream.data() + readLen, stream.size() - readLen, &readLen,
output.data(), output.size(), &decodedLen);
REQUIRE(result == returnvalue::OK);
CHECK(readLen == to1.size());
REQUIRE(decodedLen == ti1.size());
CHECK(std::equal(output.begin(), output.end(), ti1.begin()));
}
// without a delimiter the frame may simply still be arriving, so nothing may be consumed
TEST_CASE("Incomplete frame is not consumed", "[cobs]") {
std::vector<uint8_t> output(16);
size_t readLen = 0xdead;
size_t decodedLen = 0xdead;
// a self-consistent frame that is merely missing its delimiter
std::vector<uint8_t> input{3, 1, 2, 2, 3};
auto result = CobsEncoder::decode(input.data(), input.size(), &readLen, output.data(),
output.size(), &decodedLen);
CHECK(result == CobsEncoder::STREAM_TOO_SHORT);
CHECK(readLen == 0);
CHECK(decodedLen == 0);
// once the delimiter arrives, the very same bytes decode
input.push_back(0);
result = CobsEncoder::decode(input.data(), input.size(), &readLen, output.data(), output.size(),
&decodedLen);
REQUIRE(result == returnvalue::OK);
CHECK(readLen == input.size());
CHECK(decodedLen == ti1.size());
}
// an empty payload survives a round trip and is reported as a zero length frame
TEST_CASE("Empty frame", "[cobs]") {
std::vector<uint8_t> encoded(CobsEncoder::worstCaseEncodedLen(0));
size_t encodedLen = 0;
auto result = CobsEncoder::encode(nullptr, 0, encoded.data(), encoded.size(), &encodedLen);
REQUIRE(result == returnvalue::OK);
REQUIRE(encodedLen == 2);
CHECK(encoded[0] == 0x01);
CHECK(encoded[1] == 0x00);
std::vector<uint8_t> output(4);
size_t readLen = 0;
size_t decodedLen = 0;
result = CobsEncoder::decode(encoded.data(), encodedLen, &readLen, output.data(), output.size(),
&decodedLen);
CHECK(result == returnvalue::OK);
CHECK(readLen == 2);
CHECK(decodedLen == 0);
}