Files
fsfw/unittests/cfdp/pdu/testMetadataPdu.cpp
T
Tobias BaumgartlandClaude Opus 5 9d609b6d2d cfdp: exclude the PDU CRC from variable length PDU tails
FileDataReader takes the two CRC bytes out of the parsed range before it
reads its payload. The three readers with a variable length tail did not,
and each of them walks that tail until the range is exhausted:
MetadataPduReader over its option TLVs, NakPduReader over its segment
requests, FinishPduReader over its filestore and fault location TLVs.

With crcOnTransmission set at the sender, the CRC is therefore parsed as
one more TLV or segment request and the PDU is rejected, metadata and
Finished with INVALID_TLV_TYPE. Reception of CRC bearing PDUs only ever
worked for the PDUs which have no tail at all, which is why it went
unnoticed: a plain file uplink's metadata carries no options. A proxy put
request always carries one, and in acknowledged mode so do the NAK and
Finished PDUs a ground source sends, so this broke the OBSW as a
destination for any request carrying a message to user, and as a source
for every acknowledged downlink.

MetadataPduReader had a partial guard for this - an early return when the
CRC was the only thing left - which covered the no-options case and hid
the defect for the case that has them. It is replaced by the same up front
subtraction the other two now do.

The tests build CRC bearing PDUs by hand through the new PduCrcHelper,
because the creators cannot produce one: they append the CRC without
counting it in the directive data field length, which is a separate defect
left alone here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4eBFnanCACYWdKCzhHMcC
2026-09-16 10:38:08 +02:00

281 lines
12 KiB
C++

#include <array>
#include <catch2/catch_test_macros.hpp>
#include <iostream>
#include "PduCrcHelper.h"
#include "fsfw/cfdp/pdu/MetadataPduCreator.h"
#include "fsfw/cfdp/pdu/MetadataPduReader.h"
#include "fsfw/cfdp/tlv/FilestoreResponseTlv.h"
#include "fsfw/cfdp/tlv/MessageToUserTlv.h"
TEST_CASE("Metadata PDU", "[cfdp][pdu]") {
using namespace cfdp;
ReturnValue_t result;
std::array<uint8_t, 256> mdBuffer = {};
uint8_t* buffer = mdBuffer.data();
size_t sz = 0;
EntityId destId(WidthInBytes::TWO_BYTES, 2);
TransactionSeqNum seqNum(WidthInBytes::TWO_BYTES, 15);
EntityId sourceId(WidthInBytes::TWO_BYTES, 1);
PduConfig pduConf(sourceId, destId, TransmissionMode::ACKNOWLEDGED, seqNum);
std::string firstFileName = "hello.txt";
cfdp::StringLv sourceFileName(firstFileName);
cfdp::StringLv destFileName;
Fss fileSize(35);
MetadataGenericInfo info(false, ChecksumType::MODULAR, fileSize);
FilestoreResponseTlv response(FilestoreActionCode::CREATE_DIRECTORY, FSR_CREATE_NOT_ALLOWED,
sourceFileName, nullptr);
std::array<uint8_t, 3> msg = {0x41, 0x42, 0x43};
cfdp::Tlv responseTlv;
std::array<uint8_t, 64> responseBuf = {};
response.convertToTlv(responseTlv, buffer, responseBuf.size(), SerializeIF::Endianness::MACHINE);
MessageToUserTlv msgToUser(msg.data(), msg.size());
std::array<Tlv*, 2> options{&responseTlv, &msgToUser};
std::array<Tlv, 5> tlvDeser{};
REQUIRE(options[0]->getSerializedSize() == 2 + 1 + 10 + 1);
REQUIRE(options[1]->getSerializedSize() == 5);
auto metadataCheckPartOne = [&]() {
REQUIRE(mdBuffer[10] == FileDirective::METADATA);
// no closure requested and checksum type is modular => 0x00
REQUIRE(mdBuffer[11] == 0x00);
uint32_t fileSizeRaw = 0;
result = SerializeAdapter::deSerialize(&fileSizeRaw, mdBuffer.data() + 12, nullptr,
SerializeIF::Endianness::NETWORK);
REQUIRE(result == returnvalue::OK);
REQUIRE(fileSizeRaw == 35);
REQUIRE(mdBuffer[16] == 9);
REQUIRE(mdBuffer[17] == 'h');
REQUIRE(mdBuffer[18] == 'e');
REQUIRE(mdBuffer[19] == 'l');
REQUIRE(mdBuffer[20] == 'l');
REQUIRE(mdBuffer[21] == 'o');
REQUIRE(mdBuffer[22] == '.');
REQUIRE(mdBuffer[23] == 't');
REQUIRE(mdBuffer[24] == 'x');
REQUIRE(mdBuffer[25] == 't');
};
SECTION("Serialize with empty dest name") {
MetadataPduCreator serializer(pduConf, info, sourceFileName, destFileName, nullptr, 0);
result = serializer.serialize(&buffer, &sz, mdBuffer.size(), SerializeIF::Endianness::NETWORK);
REQUIRE(result == returnvalue::OK);
CHECK(sz == serializer.getSerializedSize());
// 10 byte heater + 1 byte PDU directive field + 1 byte PDU content + FSS field (4) + source
// name field (10) + dest name field (1).
REQUIRE(serializer.getWholePduSize() == 27);
REQUIRE(serializer.getSourceFileName().getSerializedSize() == 10);
REQUIRE(serializer.getDestFileName().getSerializedSize() == 1);
REQUIRE(info.getSerializedSize() == 5);
REQUIRE((mdBuffer[1] << 8 | mdBuffer[2]) == 17);
REQUIRE(serializer.getSerializedSize() == serializer.getWholePduSize());
metadataCheckPartOne();
REQUIRE(mdBuffer[26] == 0);
}
SECTION("Serialize with dest name") {
std::string secondFileName = "hello2.txt";
cfdp::StringLv destFileName2(secondFileName);
MetadataPduCreator serializer(pduConf, info, sourceFileName, destFileName2, nullptr, 0);
result = serializer.serialize(&buffer, &sz, mdBuffer.size(), SerializeIF::Endianness::NETWORK);
REQUIRE(result == returnvalue::OK);
// 10 byte heater + 1 byte PDU directive field + 1 byte PDU content + FSS field (4) + source
// name field (10) + dest name field (11).
REQUIRE(serializer.getWholePduSize() == 37);
CHECK(sz == serializer.getSerializedSize());
REQUIRE((mdBuffer[1] << 8 | mdBuffer[2]) == 27);
REQUIRE(serializer.getSerializedSize() == serializer.getWholePduSize());
metadataCheckPartOne();
// Size of destination name field
REQUIRE(mdBuffer[26] == 10);
REQUIRE(mdBuffer[27] == 'h');
REQUIRE(mdBuffer[28] == 'e');
REQUIRE(mdBuffer[29] == 'l');
REQUIRE(mdBuffer[30] == 'l');
REQUIRE(mdBuffer[31] == 'o');
REQUIRE(mdBuffer[32] == '2');
REQUIRE(mdBuffer[33] == '.');
REQUIRE(mdBuffer[34] == 't');
REQUIRE(mdBuffer[35] == 'x');
REQUIRE(mdBuffer[36] == 't');
}
SECTION("Serialize with 2 options") {
std::string otherFileName = "hello2.txt";
cfdp::StringLv otherFileNameLv(otherFileName.data(), otherFileName.size());
MetadataPduCreator serializer(pduConf, info, otherFileNameLv, destFileName, options.data(),
options.size());
info.setChecksumType(cfdp::ChecksumType::CRC_32C);
info.setClosureRequested(true);
serializer.updateDirectiveFieldLen();
result = serializer.serialize(&buffer, &sz, mdBuffer.size(), SerializeIF::Endianness::NETWORK);
REQUIRE(result == returnvalue::OK);
REQUIRE((mdBuffer[1] << 8 | mdBuffer[2]) == 37);
auto checksumType = static_cast<cfdp::ChecksumType>(mdBuffer[11] & 0x0f);
REQUIRE(checksumType == cfdp::ChecksumType::CRC_32C);
bool closureRequested = mdBuffer[11] >> 6 & 0x01;
REQUIRE(closureRequested == true);
// The size of the two options is 19. Summing up:
// - 11 bytes of source file name
// - 1 byte for dest file name
// - 4 for FSS
// - 1 leading byte.
// - 1 byte for PDU type
// PDU header has 10 bytes.
// I am not going to check the options raw content, those are part of the dedicated
// TLV unittests
REQUIRE(sz == 10 + 37);
for (size_t maxSz = 0; maxSz < sz; maxSz++) {
buffer = mdBuffer.data();
sz = 0;
result = serializer.serialize(&buffer, &sz, maxSz, SerializeIF::Endianness::NETWORK);
REQUIRE(result == SerializeIF::BUFFER_TOO_SHORT);
}
for (size_t initSz = 1; initSz < 47; initSz++) {
buffer = mdBuffer.data();
sz = initSz;
result = serializer.serialize(&buffer, &sz, 46, SerializeIF::Endianness::NETWORK);
REQUIRE(result == SerializeIF::BUFFER_TOO_SHORT);
}
}
SECTION("Deserialize") {
MetadataPduCreator serializer(pduConf, info, sourceFileName, destFileName, nullptr, 0);
result = serializer.serialize(&buffer, &sz, mdBuffer.size(), SerializeIF::Endianness::NETWORK);
REQUIRE(result == returnvalue::OK);
MetadataPduReader deserializer(mdBuffer.data(), mdBuffer.size(), info, nullptr, 0);
result = deserializer.parseData();
REQUIRE(result == returnvalue::OK);
size_t fullSize = deserializer.getWholePduSize();
for (size_t maxSz = 0; maxSz < fullSize; maxSz++) {
MetadataPduReader invalidSzDeser(mdBuffer.data(), maxSz, info, nullptr, 0);
result = invalidSzDeser.parseData();
REQUIRE(result != returnvalue::OK);
}
}
SECTION("Deserialize with 2 options") {
MetadataPduCreator serializer(pduConf, info, sourceFileName, destFileName, options.data(),
options.size());
info.setChecksumType(cfdp::ChecksumType::CRC_32C);
info.setClosureRequested(true);
serializer.updateDirectiveFieldLen();
result = serializer.serialize(&buffer, &sz, mdBuffer.size(), SerializeIF::Endianness::NETWORK);
REQUIRE(result == returnvalue::OK);
MetadataPduReader deserializer2(mdBuffer.data(), mdBuffer.size(), info, tlvDeser.data(),
tlvDeser.max_size());
result = deserializer2.parseData();
REQUIRE(result == returnvalue::OK);
REQUIRE(options[0]->getType() == cfdp::TlvType::FILESTORE_RESPONSE);
REQUIRE(options[0]->getSerializedSize() == 14);
REQUIRE(options[1]->getType() == cfdp::TlvType::MSG_TO_USER);
REQUIRE(options[1]->getSerializedSize() == 5);
for (size_t invalidFieldLen = 0; invalidFieldLen < 36; invalidFieldLen++) {
mdBuffer[1] = (invalidFieldLen >> 8) & 0xff;
mdBuffer[2] = invalidFieldLen & 0xff;
if (invalidFieldLen == 17) {
volatile uint32_t dummy = 0;
}
result = deserializer2.parseData();
if (invalidFieldLen == 17) {
REQUIRE(deserializer2.getNumberOfParsedOptions() == 0);
}
if (invalidFieldLen == 31) {
REQUIRE(deserializer2.getNumberOfParsedOptions() == 1);
}
// This is the precise length where there are no options or one option
if (invalidFieldLen != 17 and invalidFieldLen != 31) {
REQUIRE(result != returnvalue::OK);
}
}
mdBuffer[1] = (36 >> 8) & 0xff;
mdBuffer[2] = 36 & 0xff;
}
SECTION("Can not parse options") {
MetadataPduCreator serializer(pduConf, info, sourceFileName, destFileName, options.data(),
options.size());
info.setChecksumType(cfdp::ChecksumType::CRC_32C);
info.setClosureRequested(true);
buffer = mdBuffer.data();
sz = 0;
serializer.updateDirectiveFieldLen();
// info.setSourceFileName(sourceFileName);
result = serializer.serialize(&buffer, &sz, mdBuffer.size(), SerializeIF::Endianness::NETWORK);
REQUIRE(result == returnvalue::OK);
MetadataPduReader deserializer2(mdBuffer.data(), mdBuffer.size(), info, nullptr, 0);
REQUIRE(deserializer2.parseData() == cfdp::METADATA_CANT_PARSE_OPTIONS);
for (size_t maxSz = 0; maxSz < 46; maxSz++) {
MetadataPduReader invalidSzDeser(mdBuffer.data(), maxSz, info, nullptr, 0);
if (not invalidSzDeser.isNull()) {
result = invalidSzDeser.parseData();
REQUIRE(result == SerializeIF::STREAM_TOO_SHORT);
}
}
}
}
TEST_CASE("Metadata PDU with CRC", "[cfdp][pdu]") {
using namespace cfdp;
std::array<uint8_t, 256> mdBuffer = {};
uint8_t* buffer = mdBuffer.data();
size_t sz = 0;
EntityId destId(WidthInBytes::TWO_BYTES, 2);
TransactionSeqNum seqNum(WidthInBytes::TWO_BYTES, 15);
EntityId sourceId(WidthInBytes::TWO_BYTES, 1);
PduConfig pduConf(sourceId, destId, TransmissionMode::ACKNOWLEDGED, seqNum);
Fss fileSize(0);
MetadataGenericInfo info(false, ChecksumType::NULL_CHECKSUM, fileSize);
std::array<Tlv, 5> tlvDeser{};
SECTION("Metadata only with message to user") {
// This is the shape of a proxy put request: no file names, the request itself travels in a
// message to user TLV. Parsing it used to fail with INVALID_TLV_TYPE, because the option loop
// ran into the CRC and deserialized it as a further TLV.
cfdp::StringLv emptySourceName;
cfdp::StringLv emptyDestName;
std::array<uint8_t, 3> msg = {0x41, 0x42, 0x43};
MessageToUserTlv msgToUser(msg.data(), msg.size());
std::array<Tlv*, 1> options{&msgToUser};
MetadataPduCreator creator(pduConf, info, emptySourceName, emptyDestName, options.data(),
options.size());
creator.updateDirectiveFieldLen();
REQUIRE(creator.serialize(&buffer, &sz, mdBuffer.size(), SerializeIF::Endianness::NETWORK) ==
returnvalue::OK);
cfdp::test::addPduCrc(mdBuffer.data(), sz);
MetadataPduReader reader(mdBuffer.data(), sz, info, tlvDeser.data(), tlvDeser.max_size());
REQUIRE(reader.parseData() == returnvalue::OK);
REQUIRE(reader.getCrcFlag());
REQUIRE(reader.getNumberOfParsedOptions() == 1);
REQUIRE(tlvDeser[0].getType() == cfdp::TlvType::MSG_TO_USER);
REQUIRE(tlvDeser[0].getLengthField() == msg.size());
}
SECTION("No options") {
// The CRC is all that follows the file names here, which the reader handled correctly even
// before the option loop was taught about it.
std::string name = "hello.txt";
cfdp::StringLv sourceFileName(name);
cfdp::StringLv destFileName(name);
MetadataPduCreator creator(pduConf, info, sourceFileName, destFileName, nullptr, 0);
creator.updateDirectiveFieldLen();
REQUIRE(creator.serialize(&buffer, &sz, mdBuffer.size(), SerializeIF::Endianness::NETWORK) ==
returnvalue::OK);
cfdp::test::addPduCrc(mdBuffer.data(), sz);
MetadataPduReader reader(mdBuffer.data(), sz, info, tlvDeser.data(), tlvDeser.max_size());
REQUIRE(reader.parseData() == returnvalue::OK);
REQUIRE(reader.getNumberOfParsedOptions() == 0);
}
}