| #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 |