blob: 8b26cf6310612f697c403c8c87fdf2edfb6e5d4c [file] [edit]
#include "bej_common_test.hpp"
#include "bej_decoder_json.hpp"
#include "bej_deferred_binding_format.hpp"
#include "bej_encoder_json.hpp"
#include <memory>
#include <string_view>
#include <vector>
#include <gmock/gmock-matchers.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace libbej
{
struct BejDecoderTestParams
{
const std::string testName;
const BejTestInputFiles inputFiles;
};
void PrintTo(const BejDecoderTestParams& params, std::ostream* os)
{
*os << params.testName;
}
using BejDecoderTest = testing::TestWithParam<BejDecoderTestParams>;
const BejTestInputFiles driveOemTestFiles = {
.jsonFile = "../test/json/drive_oem.json",
.schemaDictionaryFile = "../test/dictionaries/drive_oem_dict.bin",
.annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
.errorDictionaryFile = "",
.encodedStreamFile = "../test/encoded/drive_oem_enc.bin",
};
const BejTestInputFiles circuitTestFiles = {
.jsonFile = "../test/json/circuit.json",
.schemaDictionaryFile = "../test/dictionaries/circuit_dict.bin",
.annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
.errorDictionaryFile = "",
.encodedStreamFile = "../test/encoded/circuit_enc.bin",
};
const BejTestInputFiles storageTestFiles = {
.jsonFile = "../test/json/storage.json",
.schemaDictionaryFile = "../test/dictionaries/storage_dict.bin",
.annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
.errorDictionaryFile = "",
.encodedStreamFile = "../test/encoded/storage_enc.bin",
};
const BejTestInputFiles dummySimpleTestFiles = {
.jsonFile = "../test/json/dummysimple.json",
.schemaDictionaryFile = "../test/dictionaries/dummy_simple_dict.bin",
.annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
.errorDictionaryFile = "",
.encodedStreamFile = "../test/encoded/dummy_simple_enc.bin",
};
// Build a BejDictionaries view over already-loaded test inputs.
BejDictionaries makeDictionaries(const BejTestInputs& inputs)
{
return BejDictionaries{
.schemaDictionary = inputs.schemaDictionary,
.schemaDictionarySize = inputs.schemaDictionarySize,
.annotationDictionary = inputs.annotationDictionary,
.annotationDictionarySize = inputs.annotationDictionarySize,
.errorDictionary = inputs.errorDictionary,
.errorDictionarySize = inputs.errorDictionarySize,
};
}
TEST_P(BejDecoderTest, Decode)
{
const BejDecoderTestParams& test_case = GetParam();
auto inputsOrErr = loadInputs(test_case.inputFiles);
EXPECT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream), 0);
std::string decoded = decoder.getOutput();
nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);
// Just comparing nlohmann::json types could lead to errors. It compares the
// byte values. So int64 and unit64 comparisons might be incorrect. Eg:
// bytes values for -5 and 18446744073709551611 are the same. So compare the
// string values.
EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
}
/**
* TODO: Add more test cases.
* - Test Enums inside array elements
* - Array inside an array: is this a valid case?
* - Real numbers with exponent part
* - Every type inside an array.
*/
INSTANTIATE_TEST_SUITE_P(
, BejDecoderTest,
testing::ValuesIn<BejDecoderTestParams>({
{"DriveOEM", driveOemTestFiles},
{"Circuit", circuitTestFiles},
{"Storage", storageTestFiles},
{"DummySimple", dummySimpleTestFiles},
}),
[](const testing::TestParamInfo<BejDecoderTest::ParamType>& info) {
return info.param.testName;
});
TEST(BejDecoderSecurityTest, MaxOperationsLimit)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
// Each array element below consists of a set and two properties, resulting
// in 3 operations. 400,000 elements will result in 1,200,000 operations,
// which should exceed the limit of 1,000,000.
constexpr int numElements = 400000;
auto root = std::make_unique<RedfishPropertyParent>();
bejTreeInitSet(root.get(), "DummySimple");
auto childArray = std::make_unique<RedfishPropertyParent>();
bejTreeInitArray(childArray.get(), "ChildArrayProperty");
bejTreeLinkChildToParent(root.get(), childArray.get());
std::vector<std::unique_ptr<RedfishPropertyParent>> sets;
std::vector<std::unique_ptr<RedfishPropertyLeafBool>> bools;
std::vector<std::unique_ptr<RedfishPropertyLeafEnum>> enums;
sets.reserve(numElements);
bools.reserve(numElements);
enums.reserve(numElements);
for (int i = 0; i < numElements; ++i)
{
auto chArraySet = std::make_unique<RedfishPropertyParent>();
bejTreeInitSet(chArraySet.get(), nullptr);
auto chArraySetBool = std::make_unique<RedfishPropertyLeafBool>();
bejTreeAddBool(chArraySet.get(), chArraySetBool.get(), "AnotherBoolean",
true);
auto chArraySetLs = std::make_unique<RedfishPropertyLeafEnum>();
bejTreeAddEnum(chArraySet.get(), chArraySetLs.get(), "LinkStatus",
"NoLink");
bejTreeLinkChildToParent(childArray.get(), chArraySet.get());
sets.push_back(std::move(chArraySet));
bools.push_back(std::move(chArraySetBool));
enums.push_back(std::move(chArraySetLs));
}
libbej::BejEncoderJson encoder;
encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
std::vector<uint8_t> outputBuffer = encoder.getOutput();
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
bejErrorNotSupported);
}
TEST(BejDecoderSecurityTest, RealWithTooManyLeadingZeros)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
auto root = std::make_unique<RedfishPropertyParent>();
bejTreeInitSet(root.get(), "DummySimple");
// 1.003 was randomely chosen
auto real = std::make_unique<RedfishPropertyLeafReal>();
bejTreeAddReal(root.get(), real.get(), "SampleRealProperty", 1.003);
libbej::BejEncoderJson encoder;
encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
std::vector<uint8_t> outputBuffer = encoder.getOutput();
// Manually tamper with the encoded stream to create the attack vector.
// We will find the `bejReal` property and overwrite its `zeroCount`.
// The property "SampleRealProperty" has sequence number 4. The encoded
// sequence number is `(4 << 1) | 0 = 8`. The nnint for 8 is `0x01, 0x08`.
const std::vector<uint8_t> realPropSeqNum = {0x01, 0x08};
auto it = std::search(outputBuffer.begin(), outputBuffer.end(),
realPropSeqNum.begin(), realPropSeqNum.end());
ASSERT_NE(it, outputBuffer.end()) << "Could not find bejReal property";
// The structure of a bejReal SFLV is: S(nnint) F(u8) L(nnint) V(...)
// The structure of V is: nnint(len(whole)), int(whole), nnint(zeroCount)...
// Find the start of the value (V) by skipping S, F, and L.
size_t sflvOffset = std::distance(outputBuffer.begin(), it);
const uint8_t* streamPtr = outputBuffer.data() + sflvOffset;
// Skip S
streamPtr += bejGetNnintSize(streamPtr);
// Skip F
streamPtr++;
// Skip L
const uint8_t* valuePtr = streamPtr + bejGetNnintSize(streamPtr);
// Find the start of zeroCount within V.
const uint8_t* zeroCountPtr = valuePtr + bejGetNnintSize(valuePtr);
zeroCountPtr += bejGetNnint(valuePtr); // Skip int(whole)
// The original zeroCount for 1.003 is 2. nnint(2) is `0x01, 0x02`.
// We replace it with a zeroCount of 101, which exceeds the limit.
// nnint(101) is `0x01, 101`. The size is the same (2 bytes), so we don't
// need to update the SFLV length field (L).
ASSERT_EQ(bejGetNnint(zeroCountPtr), 2);
size_t zeroCountOffset = zeroCountPtr - outputBuffer.data();
// nnint value for 101
outputBuffer[zeroCountOffset + 1] = 101;
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
bejErrorInvalidSize);
}
TEST(BejDecoderSecurityTest, StringTooLong)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
auto root = std::make_unique<RedfishPropertyParent>();
bejTreeInitSet(root.get(), "DummySimple");
// Create a string with a length greater than MAX_BEJ_STRING_LEN (65536).
std::string longString(65537, 'A');
auto stringProp = std::make_unique<RedfishPropertyLeafString>();
bejTreeAddString(root.get(), stringProp.get(), "Id", longString.c_str());
libbej::BejEncoderJson encoder;
encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
std::vector<uint8_t> outputBuffer = encoder.getOutput();
// The decoder should return an error because the string is too long.
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
bejErrorInvalidSize);
}
TEST(BejDecoderSecurityTest, ValueBeyondStreamLength)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
auto root = std::make_unique<RedfishPropertyParent>();
bejTreeInitSet(root.get(), "DummySimple");
auto intProp = std::make_unique<RedfishPropertyLeafInt>();
bejTreeAddInteger(root.get(), intProp.get(), "SampleIntegerProperty", 123);
libbej::BejEncoderJson encoder;
encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
std::vector<uint8_t> outputBuffer = encoder.getOutput();
// Tamper with the encoded stream to simulate a value extending beyond the
// stream length. This stream only has an integer 0x7b. The value before it
// is the length tuple.
outputBuffer[outputBuffer.size() - 2] = 0x05;
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
bejErrorInvalidSize);
}
TEST(BejDecoderSecurityTest, MalformedRootSequenceSizeTooLarge)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = makeDictionaries(*inputsOrErr);
// byte[0]=0xFF claims a 255-byte sequence number, far past this 5-byte
// buffer; must be rejected before bejGetNnint reads the sequence value.
std::vector<uint8_t> malformed = {0xFF, 0x00, 0x00, 0x00, 0x00};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(malformed)),
bejErrorInvalidSize);
}
TEST(BejDecoderSecurityTest, MalformedRootValueLengthSizeTooLarge)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = makeDictionaries(*inputsOrErr);
// seq size=1, seq=0, format=0, then byte[3]=0xFF claims a 255-byte value
// length that runs past this 5-byte buffer.
std::vector<uint8_t> malformed = {0x01, 0x00, 0x00, 0xFF, 0x00};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(malformed)),
bejErrorInvalidSize);
}
TEST(BejDecoderSecurityTest, MalformedRootHeaderTruncated)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = makeDictionaries(*inputsOrErr);
// byte[0]=0x0A pushes the value-length size byte to index 12, past this
// 6-byte buffer, even though it clears the minimum-root-size check.
std::vector<uint8_t> malformed = {0x0A, 0x00, 0x00, 0x00, 0x00, 0x00};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(malformed)),
bejErrorInvalidSize);
}
TEST(BejDecoderSecurityTest, InvalidSchemaDictionarySize)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = 10,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
bejErrorInvalidSize);
}
TEST(BejDecoderSecurityTest, InvalidAnnotationDictionarySize)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = 10,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
bejErrorInvalidSize);
}
TEST(BejDecoderSecurityTest, InvalidErrorDictionarySize)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = 10,
};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
bejErrorInvalidSize);
}
TEST(BejDecoderResourceLinkTest, DecodeResourceLink)
{
// Test that ResourceLink is decoded as "%L<ID>" format.
// Manually craft a BEJ encoded stream with a ResourceLink property.
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
// Manually construct BEJ encoded stream:
// Format byte = principalDataType << 4, so bejResourceLink(14) = 0xE0
//
// Structure:
// - PLDM header (7 bytes)
// - Root Set: S(2) + F(1) + L(2) + V(9) where V = count(2) + child(7)
// - Child ResourceLink: S(2) + F(1) + L(2) + V(2) = 7 bytes
std::vector<uint8_t> encodedStream = {
// PLDM header (7 bytes)
0x00,
0xF0,
0xF0,
0xF1, // bejVersion (0xF1F0F000 little endian)
0x00,
0x00, // reserved
0x00, // schemaClass (major)
// Root Set (DummySimple, seq=0)
0x01,
0x00, // S: nnint(0) -> seq=0
0x00, // F: bejSet (0 << 4 = 0x00)
0x01,
0x09, // L: nnint(9) -> value is 9 bytes
// Set value: element count + child SFLV
0x01,
0x01, // element count: nnint(1) -> 1 child
// Child ResourceLink (Id, seq=1)
0x01,
0x02, // S: nnint(2) -> seq=1, schema=primary
0xE0, // F: bejResourceLink (14 << 4 = 0xE0)
0x01,
0x02, // L: nnint(2) -> value is 2 bytes
0x01,
0x2A, // V: nnint(42) -> PDR ID = 42
};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(encodedStream)), 0);
std::string decoded = decoder.getOutput();
nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);
// Verify the ResourceLink is decoded as "%L42"
EXPECT_TRUE(jsonDecoded.contains("Id"));
EXPECT_EQ(jsonDecoded["Id"].get<std::string>(), "%L42");
}
// Same ResourceLink (PDR id 42) as DecodeResourceLink, exercising the two
// resolution modes a bindings map selects.
static std::vector<uint8_t> resourceLink42Stream()
{
return {
// PLDM header (7 bytes)
0x00,
0xF0,
0xF0,
0xF1, // bejVersion
0x00,
0x00, // reserved
0x00, // schemaClass (major)
// Root Set (DummySimple, seq=0)
0x01,
0x00, // S: seq=0
0x00, // F: bejSet
0x01,
0x09, // L: 9 bytes
0x01,
0x01, // element count: 1 child
// Child ResourceLink (Id, seq=1)
0x01,
0x02, // S: seq=1
0xE0, // F: bejResourceLink
0x01,
0x02, // L: 2 bytes
0x01,
0x2A, // V: PDR ID = 42
};
}
TEST(BejDecoderResourceLinkTest, DecodeResourceLinkResolvesUri)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
std::vector<uint8_t> encodedStream = resourceLink42Stream();
// Map contains resource 42: the link resolves to its URI.
BejDeferredBindingMap bindings;
bindings[bejBinding::resourceLink(42)] = "/redfish/v1/Systems/1";
BejDecoderJson decoder;
EXPECT_THAT(
decoder.decode(dictionaries, std::span(encodedStream), bindings), 0);
nlohmann::json jsonDecoded = nlohmann::json::parse(decoder.getOutput());
EXPECT_EQ(jsonDecoded["Id"].get<std::string>(), "/redfish/v1/Systems/1");
}
TEST(BejDecoderResourceLinkTest, DecodeResourceLinkUnrecognizedIsInvalidForm)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
std::vector<uint8_t> encodedStream = resourceLink42Stream();
// Non-empty map without resource 42: resolution is enabled, so the link
// takes the DSP0218 Table 42 invalid form rather than the raw "%L42".
BejDeferredBindingMap unrelated;
unrelated[bejBinding::resourceLink(99)] = "/redfish/v1/unrelated";
BejDecoderJson decoder;
EXPECT_THAT(
decoder.decode(dictionaries, std::span(encodedStream), unrelated), 0);
nlohmann::json jsonDecoded = nlohmann::json::parse(decoder.getOutput());
EXPECT_EQ(jsonDecoded["Id"].get<std::string>(), "/invalid.PDR42");
}
TEST(BejDecoderResourceLinkTest, DecodeResourceLinkNull)
{
// Test that ResourceLink with zero length is decoded as null.
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
// Same structure but with zero-length value (null ResourceLink)
// Child SFLV with L=0: S(2) + F(1) + L(2) + V(0) = 5 bytes
// Set value: count(2) + child(5) = 7 bytes
std::vector<uint8_t> encodedStream = {
// PLDM header (7 bytes)
0x00,
0xF0,
0xF0,
0xF1, // bejVersion
0x00,
0x00, // reserved
0x00, // schemaClass
// Root Set (DummySimple, seq=0)
0x01,
0x00, // S: nnint(0) -> seq=0
0x00, // F: bejSet
0x01,
0x07, // L: nnint(7) -> value is 7 bytes
// Set value: element count + child SFLV
0x01,
0x01, // element count: nnint(1) -> 1 child
// Child ResourceLink (Id, seq=1) with null value
0x01,
0x02, // S: nnint(2) -> seq=1
0xE0, // F: bejResourceLink (14 << 4 = 0xE0)
0x01,
0x00, // L: nnint(0) -> zero length means null
};
BejDecoderJson decoder;
EXPECT_THAT(decoder.decode(dictionaries, std::span(encodedStream)), 0);
std::string decoded = decoder.getOutput();
nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);
// Verify the ResourceLink is decoded as null
EXPECT_TRUE(jsonDecoded.contains("Id"));
EXPECT_TRUE(jsonDecoded["Id"].is_null());
}
TEST(BejDecoderPayloadLengthTest, IgnoresTrailingBytes)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
// Decode the reference (un-padded) stream so we can compare against it.
BejDecoderJson refDecoder;
ASSERT_EQ(refDecoder.decode(dictionaries, inputsOrErr->encodedStream), 0);
std::string refOutput = refDecoder.getOutput();
// The decoder must derive the payload length from the root SFLV alone,
// so its output has to be byte-identical to the unpadded decoding
// regardless of how much padding is appended or what byte pattern it
// uses. Cover the common real-world cases (zero padding from a
// fixed-size PLDM buffer, 0xFF sentinel, arbitrary pattern) at several
// sizes to make the equivalence robust.
const std::vector<std::pair<size_t, uint8_t>> paddings = {
{1, 0x00}, {8, 0x00}, {64, 0x00}, // zero padding (typical PLDM)
{1, 0xFF}, {8, 0xFF}, {64, 0xFF}, // sentinel padding
{1, 0xA5}, {8, 0xA5}, // arbitrary pattern
};
for (const auto& [padSize, padByte] : paddings)
{
std::vector<uint8_t> padded(inputsOrErr->encodedStream.begin(),
inputsOrErr->encodedStream.end());
padded.insert(padded.end(), padSize, padByte);
BejDecoderJson decoder;
decoder.setTrailingDataPolicy(bejTrailingIgnore);
EXPECT_EQ(decoder.decode(dictionaries, std::span(padded)), 0)
<< "padSize=" << padSize << " padByte=" << int(padByte);
EXPECT_EQ(decoder.getOutput(), refOutput)
<< "padSize=" << padSize << " padByte=" << int(padByte);
}
}
TEST(BejDecoderPayloadLengthTest, RejectsBufferShorterThanRootTuple)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
// Truncate the buffer mid-payload. The root SFLV claims a value length
// that no longer fits, so the decoder must reject the input upfront
// rather than partially decoding it.
std::span<const uint8_t> truncated(inputsOrErr->encodedStream.data(),
inputsOrErr->encodedStream.size() - 1);
BejDecoderJson decoder;
EXPECT_EQ(decoder.decode(dictionaries, truncated), bejErrorInvalidSize);
}
TEST(BejDecoderPayloadLengthTest, IgnoresTrailingBytesByDefault)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
BejDecoderJson refDecoder;
ASSERT_EQ(refDecoder.decode(dictionaries, inputsOrErr->encodedStream), 0);
std::string refOutput = refDecoder.getOutput();
std::vector<uint8_t> padded(inputsOrErr->encodedStream.begin(),
inputsOrErr->encodedStream.end());
padded.insert(padded.end(), {0xFF, 0xFF, 0xFF, 0xFF});
// The default policy is now bejTrailingIgnore, so a buffer with bytes
// past the root SFLV's value length must decode successfully without an
// explicit policy being set, matching the unpadded decoding.
BejDecoderJson decoder;
EXPECT_EQ(decoder.decode(dictionaries, std::span(padded)), 0);
EXPECT_EQ(decoder.getOutput(), refOutput);
}
TEST(BejDecoderPayloadLengthTest, WarnsOnTrailingBytes)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
BejDecoderJson refDecoder;
ASSERT_EQ(refDecoder.decode(dictionaries, inputsOrErr->encodedStream), 0);
std::string refOutput = refDecoder.getOutput();
std::vector<uint8_t> padded(inputsOrErr->encodedStream.begin(),
inputsOrErr->encodedStream.end());
padded.insert(padded.end(), {0xFF, 0xFF, 0xFF, 0xFF});
// Warn policy must still decode successfully and produce the same
// output as the unpadded reference, while emitting a diagnostic to
// stderr that mentions the trailing bytes.
BejDecoderJson decoder;
decoder.setTrailingDataPolicy(bejTrailingWarn);
testing::internal::CaptureStderr();
EXPECT_EQ(decoder.decode(dictionaries, std::span(padded)), 0);
std::string captured = testing::internal::GetCapturedStderr();
EXPECT_EQ(decoder.getOutput(), refOutput);
EXPECT_THAT(captured, testing::HasSubstr("trailing bytes after root SFLV"));
EXPECT_THAT(captured, testing::HasSubstr("ignored"));
}
TEST(BejDecoderPayloadLengthTest, ErrorsOnTrailingBytes)
{
auto inputsOrErr = loadInputs(dummySimpleTestFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = {
.schemaDictionary = inputsOrErr->schemaDictionary,
.schemaDictionarySize = inputsOrErr->schemaDictionarySize,
.annotationDictionary = inputsOrErr->annotationDictionary,
.annotationDictionarySize = inputsOrErr->annotationDictionarySize,
.errorDictionary = inputsOrErr->errorDictionary,
.errorDictionarySize = inputsOrErr->errorDictionarySize,
};
std::vector<uint8_t> padded(inputsOrErr->encodedStream.begin(),
inputsOrErr->encodedStream.end());
padded.insert(padded.end(), {0xFF, 0xFF, 0xFF, 0xFF});
// Explicit error policy rejects the input as bejErrorInvalidSize.
BejDecoderJson strict;
strict.setTrailingDataPolicy(bejTrailingError);
EXPECT_EQ(strict.decode(dictionaries, std::span(padded)),
bejErrorInvalidSize);
}
// Fixtures captured from a real Intel E810 NIC. The BEJ payload is produced by
// the device's RDE stack (not by this library's encoder) and its string
// elements carry the deferred-binding bit, so it exercises the decode path
// against an externally generated binary. See Gerrit 91082.
const BejTestInputFiles networkAdapterDeferredBindingFiles = {
.jsonFile = "../test/json/network_adapter.json",
.schemaDictionaryFile = "../test/dictionaries/network_adapter_dict.bin",
.annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
.errorDictionaryFile = "",
.encodedStreamFile = "../test/encoded/network_adapter_enc.bin",
};
constexpr const char* networkAdapterPdrFile =
"../test/pdr/network_adapter_pdr.json";
constexpr const char* networkAdapterResolvedJsonFile =
"../test/json/network_adapter_resolved.json";
/**
* @brief Load a deferred-binding map from a pdr.json mapping table.
*
* @param[in] pdrFile - path to a JSON object whose keys are placeholders with
* the leading '%' marker (e.g. "%L1") and whose values are the
* substitutions.
* @return map keyed by token body (no '%'), matching the decoder's keys, or
* nullopt on a missing file or a malformed key.
*/
std::optional<BejDeferredBindingMap> loadBindingMap(const char* pdrFile)
{
std::ifstream pdrInput(pdrFile);
if (!pdrInput.is_open())
{
return std::nullopt;
}
nlohmann::json pdr;
pdrInput >> pdr;
BejDeferredBindingMap bindings;
for (const auto& [placeholder, uri] : pdr.items())
{
// The decoder keys on the token body; pdr.json keeps the '%' marker
// only for readability, so strip it here.
if (placeholder.empty() ||
placeholder.front() != bejDeferredBindingMarker)
{
return std::nullopt;
}
bindings.emplace(placeholder.substr(1), uri.get<std::string>());
}
return bindings;
}
// Without a binding map the decoder must leave deferred-binding placeholders
// untouched (DSP0218 8.3 passthrough), so a device payload stays decodable
// before its resource links are known.
TEST(BejDeferredBindingDecodeTest, PreservesPlaceholdersWithoutBindings)
{
auto inputsOrErr = loadInputs(networkAdapterDeferredBindingFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = makeDictionaries(*inputsOrErr);
BejDecoderJson decoder;
ASSERT_EQ(decoder.decode(dictionaries, inputsOrErr->encodedStream), 0);
nlohmann::json decoded = nlohmann::json::parse(decoder.getOutput());
// network_adapter.json still holds the "%L.."/"%I.." placeholders.
EXPECT_EQ(decoded.dump(), inputsOrErr->expectedJson.dump());
}
// With the PDR-derived map every placeholder must be substituted with its
// resource URI. The resolved golden is produced independently of libbej, so
// matching it proves the decoder applied the mapping table rather than echoing
// its own substitution back.
TEST(BejDeferredBindingDecodeTest, ResolvesPlaceholdersWithBindings)
{
auto inputsOrErr = loadInputs(networkAdapterDeferredBindingFiles);
ASSERT_TRUE(inputsOrErr);
BejDictionaries dictionaries = makeDictionaries(*inputsOrErr);
auto bindings = loadBindingMap(networkAdapterPdrFile);
ASSERT_TRUE(bindings);
std::ifstream resolvedInput(networkAdapterResolvedJsonFile);
ASSERT_TRUE(resolvedInput.is_open());
nlohmann::json expectedResolved;
resolvedInput >> expectedResolved;
BejDecoderJson decoder;
ASSERT_EQ(
decoder.decode(dictionaries, inputsOrErr->encodedStream, *bindings), 0);
nlohmann::json decoded = nlohmann::json::parse(decoder.getOutput());
EXPECT_EQ(decoded.dump(), expectedResolved.dump());
// Tie the result to the mapping table, not to a coincidental literal: the
// self link and id must equal the map entries for resource id 1.
EXPECT_EQ(decoded.at("@odata.id").get<std::string>(),
bindings->at(bejBinding::resourceLink(1)));
EXPECT_EQ(decoded.at("Id").get<std::string>(),
bindings->at(bejBinding::instanceId(1)));
}
} // namespace libbej