blob: 4d7198302f6f7c591097e23422bcb566a2b91082 [file] [edit]
#include "bej_decoder_json.hpp"
#include "bej_common.h"
#include "bej_decoder_core.h"
#include "bej_deferred_binding_format.hpp"
#include <array>
#include <charconv>
#include <cstdio>
#include <cstring>
#include <format>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#define MAX_BEJ_STRING_LEN 65536
namespace libbej
{
/**
* @brief This structure is used to pass additional data to callback functions.
*/
struct BejJsonParam
{
bool* isPrevAnnotated;
std::string* output;
const BejDeferredBindingMap* bindings;
};
/**
* @brief Add a property name to output buffer.
*
* @param[in] params - a valid BejJsonParam struct.
* @param[in] propertyName - a NULL terminated string.
*/
static void addPropertyNameToOutput(struct BejJsonParam* params,
const char* propertyName)
{
if (propertyName[0] == '\0')
{
return;
}
if (!(*params->isPrevAnnotated))
{
params->output->push_back('\"');
}
params->output->append(propertyName);
params->output->append("\":");
}
/**
* @brief Callback for bejSet start.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackSetStart(const char* propertyName, void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
addPropertyNameToOutput(params, propertyName);
params->output->push_back('{');
*params->isPrevAnnotated = false;
return 0;
}
/**
* @brief Callback for bejSet end.
*
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackSetEnd(void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
params->output->push_back('}');
return 0;
}
/**
* @brief Callback for bejArray start.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackArrayStart(const char* propertyName, void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
addPropertyNameToOutput(params, propertyName);
params->output->push_back('[');
*params->isPrevAnnotated = false;
return 0;
}
/**
* @brief Callback for bejArray end.
*
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackArrayEnd(void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
params->output->push_back(']');
return 0;
}
/**
* @brief Callback when an end of a property is detected.
*
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackPropertyEnd(void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
// Not a section ending. So add a comma.
params->output->push_back(',');
return 0;
}
/**
* @brief Callback for bejNull type.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackNull(const char* propertyName, void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
addPropertyNameToOutput(params, propertyName);
params->output->append("null");
*params->isPrevAnnotated = false;
return 0;
}
/**
* @brief Callback for bejInteger type.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] value - integer value.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackInteger(const char* propertyName, int64_t value,
void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
addPropertyNameToOutput(params, propertyName);
params->output->append(std::to_string(value));
*params->isPrevAnnotated = false;
return 0;
}
/**
* @brief Callback for bejEnum type.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] value - a NULL terminated string.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackEnum(const char* propertyName, const char* value,
void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
addPropertyNameToOutput(params, propertyName);
params->output->push_back('\"');
params->output->append(value);
params->output->push_back('\"');
*params->isPrevAnnotated = false;
return 0;
}
/**
* @brief Parse a base-10 integer at str[start] without throwing.
*
* @param[in] str - the string to parse from (start must be <= str.length()).
* @param[in] start - index to begin parsing.
* @param[out] out - parsed value, valid only on success.
* @param[out] end - index just past the last parsed digit.
* @return true if at least one digit was parsed and the value fit in T.
*/
template <typename T>
static bool parseDecimalAt(const std::string& str, size_t start, T& out,
size_t& end)
{
auto result =
std::from_chars(str.data() + start, str.data() + str.length(), out);
end = static_cast<size_t>(result.ptr - str.data());
return result.ec == std::errc{};
}
// DSP0218 Table 42 markers supported by this decoder, modelled as data:
// - a marker fitting an existing arity is just a new table row.
// - a marker with a new operand shape (e.g. %P<id>.PAGE<offset>) also needs a
// new BejMacroArity value and a parse branch below.
namespace
{
// How a marker's trailing parameters are parsed, which also fixes the token
// boundary.
enum class BejMacroArity : uint8_t
{
none, // %C, %S: the token is just the marker text
resourceId, // %L<id>, %I<id>: marker + decimal id
resourceIdDotAction, // %T<id>.<action>: marker + decimal id + '.' + action
};
// Shape of the DSP0218 Table 42 "invalid" form, emitted when resolution is
// enabled (a non-empty map) but the placeholder cannot be resolved.
enum class BejInvalidKind : uint8_t
{
none, // %C, %S: spec defines no invalid form; leave the token raw
resourcePdr, // %L<id> -> "/invalid.PDR<id>"
literal, // %I<id> -> "invalid"
action, // %T<id>.<n> -> "/invalid.<id>.<n>"
};
// A marker is matched as text, not a single character, so the table can hold
// multi-symbol markers such as "%PD":
// - findMacroSpec recognizes it; longest-match wins over a shorter prefix.
// - each new operand shape needs its own BejMacroArity and invalid-form.
struct BejMacroSpec
{
std::string_view marker;
BejMacroArity arity;
BejInvalidKind invalid;
};
// Wrap a single-character marker constant as a string_view, so the table reuses
// the marker constants instead of duplicating the character literals.
constexpr std::string_view asMarker(const char& marker)
{
return std::string_view(&marker, 1);
}
constexpr auto bejMacroSpecs = std::to_array<BejMacroSpec>({
{asMarker(bejDeferredBindingResourceLink), BejMacroArity::resourceId,
BejInvalidKind::resourcePdr},
{asMarker(bejDeferredBindingInstanceId), BejMacroArity::resourceId,
BejInvalidKind::literal},
{asMarker(bejDeferredBindingActionUri), BejMacroArity::resourceIdDotAction,
BejInvalidKind::action},
{asMarker(bejDeferredBindingChassis), BejMacroArity::none,
BejInvalidKind::none},
{asMarker(bejDeferredBindingSystem), BejMacroArity::none,
BejInvalidKind::none},
});
constexpr std::string_view bejInvalidPdrPrefix = "/invalid.PDR";
constexpr std::string_view bejInvalidLiteral = "invalid";
constexpr std::string_view bejInvalidActionPrefix = "/invalid.";
// Find the marker whose text matches str at markerStart. Longest match wins, so
// a multi-symbol marker is preferred over a single-symbol prefix of it.
const BejMacroSpec* findMacroSpec(std::string_view str, size_t markerStart)
{
const BejMacroSpec* match = nullptr;
for (const BejMacroSpec& spec : bejMacroSpecs)
{
if (str.compare(markerStart, spec.marker.length(), spec.marker) != 0)
{
continue;
}
if (match == nullptr || spec.marker.length() > match->marker.length())
{
match = &spec;
}
}
return match;
}
// Parse the placeholder body that begins at the marker text (one past the '%').
// On success, bodyEnd is the index just past the token, and resourceId /
// actionId hold the parsed numbers per the marker's arity. Returns false for a
// malformed body, which the caller leaves raw.
bool parseMacroBody(const std::string& str, size_t markerStart,
const BejMacroSpec& spec, size_t& bodyEnd,
uint32_t& resourceId, uint32_t& actionId)
{
// Trailing parameters follow the marker text; a multi-symbol marker just
// shifts where they begin.
size_t paramStart = markerStart + spec.marker.length();
if (spec.arity == BejMacroArity::none)
{
bodyEnd = paramStart;
return true;
}
// from_chars reports overflow and "no digits" via its return, unlike
// std::stoul whose exception would unwind across the C decoder frames.
size_t idEnd = 0;
if (!parseDecimalAt(str, paramStart, resourceId, idEnd))
{
return false;
}
if (spec.arity == BejMacroArity::resourceId)
{
bodyEnd = idEnd;
return true;
}
// resourceIdDotAction requires a ".<action>" suffix.
if (idEnd >= str.length() ||
str[idEnd] != bejDeferredBindingActionSeparator)
{
return false;
}
size_t actEnd = 0;
if (!parseDecimalAt(str, idEnd + 1, actionId, actEnd))
{
return false;
}
bodyEnd = actEnd;
return true;
}
// Build the DSP0218 Table 42 invalid form for an unresolved placeholder.
// Returns false when the marker has no invalid form (%C, %S), leaving it raw.
bool buildInvalidForm(const BejMacroSpec& spec, uint32_t resourceId,
uint32_t actionId, std::string& out)
{
switch (spec.invalid)
{
case BejInvalidKind::resourcePdr:
out = std::format("{}{}", bejInvalidPdrPrefix, resourceId);
return true;
case BejInvalidKind::literal:
out = std::string(bejInvalidLiteral);
return true;
case BejInvalidKind::action:
out = std::format("{}{}{}{}", bejInvalidActionPrefix, resourceId,
bejDeferredBindingActionSeparator, actionId);
return true;
case BejInvalidKind::none:
return false;
}
return false;
}
} // namespace
/**
* @brief Substitute deferred binding placeholders in a string and append the
* result to the output.
*/
static void addDeferredBindingString(struct BejJsonParam* params,
const char* value, size_t length)
{
if (length == 0)
{
return;
}
if (!params->bindings || params->bindings->empty())
{
// Resolution disabled: emit the raw string unchanged.
params->output->append(value, length - 1);
return;
}
std::string str(value, length - 1);
size_t pos = 0;
while ((pos = str.find(bejDeferredBindingMarker, pos)) != std::string::npos)
{
if (pos + 1 >= str.length())
{
// A lone '%' at the end of the string: no marker can follow.
break;
}
if (str[pos + 1] == bejDeferredBindingMarker)
{
// Escaped marker. Replace "%%" with "%".
str.replace(pos, 2, 1, bejDeferredBindingMarker);
pos++;
continue;
}
const BejMacroSpec* spec = findMacroSpec(str, pos + 1);
if (spec == nullptr)
{
// Unsupported marker: DSP0218 Table 42 passes it through unchanged.
pos++;
continue;
}
size_t bodyEnd = 0;
uint32_t resourceId = 0;
uint32_t actionId = 0;
if (!parseMacroBody(str, pos + 1, *spec, bodyEnd, resourceId, actionId))
{
// Malformed body: leave the placeholder raw.
pos++;
continue;
}
// The lookup token is the placeholder body (e.g. "L30", "T30.1", "C").
std::string token = str.substr(pos + 1, bodyEnd - (pos + 1));
std::string replacement;
auto it = params->bindings->find(token);
if (it != params->bindings->end() && !it->second.empty())
{
replacement = it->second;
}
else if (!buildInvalidForm(*spec, resourceId, actionId, replacement))
{
// No binding and no invalid form (%C, %S): leave the token raw.
pos = bodyEnd;
continue;
}
str.replace(pos, bodyEnd - pos, replacement);
pos += replacement.length();
}
params->output->append(str);
}
/**
* @brief Callback for bejString type.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] value - a NULL terminated string.
* @param[in] length - length of the string.
* @param[in] deferredBinding - indicates if string contains deferred binding
* placeholders.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackString(const char* propertyName, const char* value,
size_t length, bool deferredBinding, void* dataPtr)
{
if ((length > MAX_BEJ_STRING_LEN) ||
(strnlen(value, length) != (length - 1)))
{
fprintf(stderr,
"Incorrect BEJ string length %zu or it exceeds maximum %u.\n",
(length - 1), MAX_BEJ_STRING_LEN);
return bejErrorInvalidSize;
}
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
addPropertyNameToOutput(params, propertyName);
params->output->push_back('\"');
if (length > 0)
{
if (deferredBinding)
{
addDeferredBindingString(params, value, length);
}
else
{
params->output->append(value, length - 1);
}
}
params->output->push_back('\"');
*params->isPrevAnnotated = false;
return 0;
}
/**
* @brief Callback for bejReal type.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] value - pointing to a valid BejReal.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackReal(const char* propertyName, const struct BejReal* value,
void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
// Sanity check for zeroCount
if (value->zeroCount > 100)
{
return bejErrorInvalidSize;
}
addPropertyNameToOutput(params, propertyName);
params->output->append(std::to_string(value->whole));
params->output->push_back('.');
params->output->insert(params->output->cend(), value->zeroCount, '0');
params->output->append(std::to_string(value->fract));
if (value->expLen != 0)
{
params->output->push_back('e');
params->output->append(std::to_string(value->exp));
}
*params->isPrevAnnotated = false;
return 0;
}
/**
* @brief Callback for bejBoolean type.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] value - boolean value.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackBool(const char* propertyName, bool value, void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
addPropertyNameToOutput(params, propertyName);
params->output->append(value ? "true" : "false");
*params->isPrevAnnotated = false;
return 0;
}
/**
* @brief Callback for bejPropertyAnnotation type.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackAnnotation(const char* propertyName, void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
params->output->push_back('\"');
params->output->append(propertyName);
// bejPropertyAnnotation type has the form "Status@Message.ExtendedInfo".
// First the decoder will see "Status" part of the annotated property. This
// will be in its own SFLV tuple. The remainder of the property name,
// @Message.ExtendedInfo will be contained in the next bej SFLV tuple.
// Therefore to add the inverted commas to the complete property name,
// Status@Message.ExtendedInfo, we need to know that the previous property
// we processed is a start to an annotation property. We can use
// isPrevAnnotated to pass this information.
// Here we are adding: "propertyName
// If isPrevAnnotated is true, next property should add: propertyNameNext"
*params->isPrevAnnotated = true;
return 0;
}
/**
* @brief Callback for bejResourceLink type.
*
* @param[in] propertyName - a NULL terminated string.
* @param[in] linkId - Resource Link ID.
* @param[in] dataPtr - pointing to a valid BejJsonParam struct.
* @return 0 if successful.
*/
static int callbackResourceLink(const char* propertyName, uint64_t linkId,
void* dataPtr)
{
struct BejJsonParam* params =
reinterpret_cast<struct BejJsonParam*>(dataPtr);
addPropertyNameToOutput(params, propertyName);
*params->isPrevAnnotated = false;
// bejResourceLink carries a numeric resource id, not a placeholder string,
// so it resolves here rather than inside addDeferredBindingString.
const bool resolutionEnabled =
params->bindings && !params->bindings->empty();
if (!resolutionEnabled)
{
// Resolution disabled (no bindings supplied): preserve the raw %L<id>
// placeholder, matching decode() called without a map.
params->output->append(
std::format("\"{}{}{}\"", bejDeferredBindingMarker,
bejDeferredBindingResourceLink, linkId));
return 0;
}
// Resource ids are 32-bit; a link id beyond that range cannot name a
// resource, so it never matches and falls through to the invalid form
// rather than being truncated into a 32-bit token.
if (linkId <= std::numeric_limits<uint32_t>::max())
{
auto it = params->bindings->find(
bejBinding::resourceLink(static_cast<uint32_t>(linkId)));
if (it != params->bindings->end() && !it->second.empty())
{
params->output->append(std::format("\"{}\"", it->second));
return 0;
}
}
// Enabled but unrecognized (or out of 32-bit range): %L invalid form.
params->output->append(
std::format("\"{}{}\"", bejInvalidPdrPrefix, linkId));
return 0;
}
/**
* @brief Callback for stackEmpty.
*
* @param[in] dataPtr - pointer to a valid std::vector<BejStackProperty>
* @return true if the stack is empty.
*/
static bool stackEmpty(void* dataPtr)
{
std::vector<BejStackProperty>* stack =
reinterpret_cast<std::vector<BejStackProperty>*>(dataPtr);
return stack->empty();
}
/**
* @brief Callback for stackPeek.
*
* @param[in] dataPtr - pointer to a valid std::vector<BejStackProperty>
* @return a const reference to the stack top.
*/
static const struct BejStackProperty* stackPeek(void* dataPtr)
{
std::vector<BejStackProperty>* stack =
reinterpret_cast<std::vector<BejStackProperty>*>(dataPtr);
if (stack->empty())
{
return nullptr;
}
return &(stack->back());
}
/**
* @brief Callback for stackPop. Remove the top element from the stack.
*
* @param[in] dataPtr - pointer to a valid std::vector<BejStackProperty>
*/
static void stackPop(void* dataPtr)
{
std::vector<BejStackProperty>* stack =
reinterpret_cast<std::vector<BejStackProperty>*>(dataPtr);
if (stack->empty())
{
return;
}
stack->pop_back();
}
/**
* @brief Callback for stackPush. Push a new element to the top of the stack.
*
* @param[in] property - property to push.
* @param[in] dataPtr - pointer to a valid std::vector<BejStackProperty>
* @return 0 if successful.
*/
static int stackPush(const struct BejStackProperty* const property,
void* dataPtr)
{
std::vector<BejStackProperty>* stack =
reinterpret_cast<std::vector<BejStackProperty>*>(dataPtr);
stack->push_back(*property);
return 0;
}
int BejDecoderJson::decode(const BejDictionaries& dictionaries,
const std::span<const uint8_t> encodedPldmBlock,
const BejDeferredBindingMap& bindings)
{
// Clear the previous output if any.
output.clear();
// The dictionaries have to be traversed in a depth first manner. This is
// using a stack to implement it non-recursively. Going into a set or an
// array or a property annotation section means that we have to jump to the
// child dictionary offset start point but needs to retrieve the parent
// dictionary offset start once all the children are processed. This stack
// will hold the parent dictionary offsets and endings for each section.
stack.clear();
struct BejStackCallback stackCallback = {
.stackEmpty = stackEmpty,
.stackPeek = stackPeek,
.stackPop = stackPop,
.stackPush = stackPush,
};
struct BejDecodedCallback decodedCallback = {
.callbackSetStart = callbackSetStart,
.callbackSetEnd = callbackSetEnd,
.callbackArrayStart = callbackArrayStart,
.callbackArrayEnd = callbackArrayEnd,
.callbackPropertyEnd = callbackPropertyEnd,
.callbackNull = callbackNull,
.callbackInteger = callbackInteger,
.callbackEnum = callbackEnum,
.callbackString = callbackString,
.callbackReal = callbackReal,
.callbackBool = callbackBool,
.callbackAnnotation = callbackAnnotation,
.callbackResourceLink = callbackResourceLink,
.callbackReadonlyPropertyAndTopLevelAnnotation = nullptr,
};
isPrevAnnotated = false;
struct BejJsonParam callbackData = {
.isPrevAnnotated = &isPrevAnnotated,
.output = &output,
.bindings = &bindings,
};
return bejDecodePldmBlockWithPolicy(
&dictionaries, encodedPldmBlock.data(), encodedPldmBlock.size_bytes(),
&stackCallback, &decodedCallback, (void*)(&callbackData),
(void*)(&stack), trailingPolicy);
}
std::string BejDecoderJson::getOutput()
{
return output;
}
} // namespace libbej