blob: 0366db44330b206b07712ba76aaab8feeca14daa [file] [log] [blame]
#ifndef THIRD_PARTY_GBMCWEB_REDFISH_CORE_INCLUDE_UTILS_JSON_UTILS_H_
#define THIRD_PARTY_GBMCWEB_REDFISH_CORE_INCLUDE_UTILS_JSON_UTILS_H_
/*
// Copyright (c) 2018 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
*/
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <span> // NOLINT
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
#include "http_request.hpp"
#include "http_response.hpp"
#include "logging.hpp"
#include "human_sort.hpp"
#include "error_messages.hpp"
#include <nlohmann/json.hpp>
// IWYU pragma: no_include <stdint.h>
// IWYU pragma: no_forward_declare crow::Request
namespace redfish {
namespace json_util {
/**
* @brief Processes request to extract JSON from its body. If it fails, adds
* MalformedJSON message to response and ends it.
*
* @param[io] res Response object
* @param[in] req Request object
* @param[out] reqJson JSON object extracted from request's body
*
* @return true if JSON is valid, false when JSON is invalid and response has
* been filled with message and ended.
*/
bool processJsonFromRequest(crow::Response& res, const crow::Request& req,
nlohmann::json& reqJson);
namespace details {
template <typename Type>
struct IsOptional : std::false_type {};
template <typename Type>
struct IsOptional<std::optional<Type>> : std::true_type {};
template <typename Type>
struct IsVector : std::false_type {};
template <typename Type>
struct IsVector<std::vector<Type>> : std::true_type {};
template <typename Type>
struct IsStdArray : std::false_type {};
template <typename Type, std::size_t size>
struct IsStdArray<std::array<Type, size>> : std::true_type {};
enum class UnpackErrorCode : std::uint8_t { success, invalidType, outOfRange };
template <typename ToType, typename FromType>
bool checkRange(const FromType& from, std::string_view key) {
if (from > std::numeric_limits<ToType>::max()) {
BMCWEB_LOG_DEBUG << "Value for key " << key
<< " was greater than max: " << __PRETTY_FUNCTION__;
return false;
}
if (from < std::numeric_limits<ToType>::lowest()) {
BMCWEB_LOG_DEBUG << "Value for key " << key
<< " was less than min: " << __PRETTY_FUNCTION__;
return false;
}
if constexpr (std::is_floating_point_v<ToType>) {
if (std::isnan(from)) {
BMCWEB_LOG_DEBUG << "Value for key " << key << " was NAN";
return false;
}
}
return true;
}
template <typename Type>
UnpackErrorCode unpackValueWithErrorCode(nlohmann::json& jsonValue,
std::string_view key, Type& value) {
UnpackErrorCode ret = UnpackErrorCode::success;
if constexpr (std::is_floating_point_v<Type>) {
double helper = 0;
double* jsonPtr = jsonValue.get_ptr<double*>();
if (jsonPtr == nullptr) {
int64_t* intPtr = jsonValue.get_ptr<int64_t*>();
if (intPtr != nullptr) {
helper = static_cast<double>(*intPtr);
jsonPtr = &helper;
}
}
if (jsonPtr == nullptr) {
return UnpackErrorCode::invalidType;
}
if (!checkRange<Type>(*jsonPtr, key)) {
return UnpackErrorCode::outOfRange;
}
value = static_cast<Type>(*jsonPtr);
} else if constexpr (std::is_signed_v<Type>) {
int64_t* jsonPtr = jsonValue.get_ptr<int64_t*>();
if (jsonPtr == nullptr) {
return UnpackErrorCode::invalidType;
}
if (!checkRange<Type>(*jsonPtr, key)) {
return UnpackErrorCode::outOfRange;
}
value = static_cast<Type>(*jsonPtr);
} else if constexpr ((std::is_unsigned_v<Type>) &&
(!std::is_same_v<bool, Type>)) {
uint64_t* jsonPtr = jsonValue.get_ptr<uint64_t*>();
if (jsonPtr == nullptr) {
return UnpackErrorCode::invalidType;
}
if (!checkRange<Type>(*jsonPtr, key)) {
return UnpackErrorCode::outOfRange;
}
value = static_cast<Type>(*jsonPtr);
} else if constexpr (std::is_same_v<nlohmann::json, Type>) {
value = std::move(jsonValue);
} else {
using JsonType = std::add_const_t<std::add_pointer_t<Type>>;
JsonType jsonPtr = jsonValue.get_ptr<JsonType>();
if (jsonPtr == nullptr) {
BMCWEB_LOG_DEBUG << "Value for key " << key
<< " was incorrect type: " << jsonValue.type_name();
return UnpackErrorCode::invalidType;
}
value = std::move(*jsonPtr);
}
return ret;
}
template <typename Type>
bool unpackValue(nlohmann::json& jsonValue, std::string_view key,
crow::Response& res, Type& value) {
bool ret = true;
if constexpr (IsOptional<Type>::value) {
value.emplace();
ret = unpackValue<typename Type::value_type>(jsonValue, key, res, *value) &&
ret;
} else if constexpr (IsStdArray<Type>::value) {
if (!jsonValue.is_array()) {
messages::propertyValueTypeError(
res,
res.jsonValue.dump(2, ' ', true,
nlohmann::json::error_handler_t::replace),
key);
return false;
}
if (jsonValue.size() != value.size()) {
messages::propertyValueTypeError(
res,
res.jsonValue.dump(2, ' ', true,
nlohmann::json::error_handler_t::replace),
key);
return false;
}
size_t index = 0;
for (const auto& val : jsonValue.items()) {
ret = unpackValue<typename Type::value_type>(val.value(), key, res,
value[index++]) &&
ret;
}
} else if constexpr (IsVector<Type>::value) {
if (!jsonValue.is_array()) {
messages::propertyValueTypeError(
res,
res.jsonValue.dump(2, ' ', true,
nlohmann::json::error_handler_t::replace),
key);
return false;
}
for (const auto& val : jsonValue.items()) {
value.emplace_back();
ret = unpackValue<typename Type::value_type>(val.value(), key, res,
value.back()) &&
ret;
}
} else {
UnpackErrorCode ec = unpackValueWithErrorCode(jsonValue, key, value);
if (ec != UnpackErrorCode::success) {
if (ec == UnpackErrorCode::invalidType) {
messages::propertyValueTypeError(
res,
jsonValue.dump(2, ' ', true,
nlohmann::json::error_handler_t::replace),
key);
} else if (ec == UnpackErrorCode::outOfRange) {
messages::propertyValueNotInList(
res,
jsonValue.dump(2, ' ', true,
nlohmann::json::error_handler_t::replace),
key);
}
return false;
}
}
return ret;
}
template <typename Type>
bool unpackValue(nlohmann::json& jsonValue, std::string_view key, Type& value) {
bool ret = true;
if constexpr (IsOptional<Type>::value) {
value.emplace();
ret = unpackValue<typename Type::value_type>(jsonValue, key, *value) && ret;
} else if constexpr (IsStdArray<Type>::value) {
if (!jsonValue.is_array()) {
return false;
}
if (jsonValue.size() != value.size()) {
return false;
}
size_t index = 0;
for (const auto& val : jsonValue.items()) {
ret = unpackValue<typename Type::value_type>(val.value(), key,
value[index++]) &&
ret;
}
} else if constexpr (IsVector<Type>::value) {
if (!jsonValue.is_array()) {
return false;
}
for (const auto& val : jsonValue.items()) {
value.emplace_back();
ret = unpackValue<typename Type::value_type>(val.value(), key,
value.back()) &&
ret;
}
} else {
UnpackErrorCode ec = unpackValueWithErrorCode(jsonValue, key, value);
if (ec != UnpackErrorCode::success) {
return false;
}
}
return ret;
}
} // namespace details
// clang-format off
using UnpackVariant = std::variant<
uint8_t*,
uint16_t*,
int16_t*,
uint32_t*,
int32_t*,
uint64_t*,
int64_t*,
bool*,
double*,
std::string*,
nlohmann::json*,
std::vector<uint8_t>*,
std::vector<uint16_t>*,
std::vector<int16_t>*,
std::vector<uint32_t>*,
std::vector<int32_t>*,
std::vector<uint64_t>*,
std::vector<int64_t>*,
// std::vector<bool>*,
std::vector<double>*,
std::vector<std::string>*,
std::vector<nlohmann::json>*,
std::optional<uint8_t>*,
std::optional<uint16_t>*,
std::optional<int16_t>*,
std::optional<uint32_t>*,
std::optional<int32_t>*,
std::optional<uint64_t>*,
std::optional<int64_t>*,
std::optional<bool>*,
std::optional<double>*,
std::optional<std::string>*,
std::optional<nlohmann::json>*,
std::optional<std::vector<uint8_t>>*,
std::optional<std::vector<uint16_t>>*,
std::optional<std::vector<int16_t>>*,
std::optional<std::vector<uint32_t>>*,
std::optional<std::vector<int32_t>>*,
std::optional<std::vector<uint64_t>>*,
std::optional<std::vector<int64_t>>*,
// std::optional<std::vector<bool>>*,
std::optional<std::vector<double>>*,
std::optional<std::vector<std::string>>*,
std::optional<std::vector<nlohmann::json>>*
>;
// clang-format on
struct PerUnpack {
std::string_view key;
UnpackVariant value;
bool complete = false;
};
inline bool readJsonHelper(nlohmann::json& jsonRequest, crow::Response& res,
std::span<PerUnpack> toUnpack) {
bool result = true;
nlohmann::json::object_t* obj =
jsonRequest.get_ptr<nlohmann::json::object_t*>();
if (obj == nullptr) {
BMCWEB_LOG_DEBUG << "Json value is not an object";
messages::unrecognizedRequestBody(res);
return false;
}
for (auto& item : *obj) {
size_t unpackIndex = 0;
for (; unpackIndex < toUnpack.size(); unpackIndex++) {
PerUnpack& unpackSpec = toUnpack[unpackIndex];
std::string_view key = unpackSpec.key;
size_t keysplitIndex = key.find('/');
std::string_view leftover;
if (keysplitIndex != std::string_view::npos) {
leftover = key.substr(keysplitIndex + 1);
key = key.substr(0, keysplitIndex);
}
if (key != item.first || unpackSpec.complete) {
continue;
}
// Sublevel key
if (!leftover.empty()) {
// Include the slash in the key so we can compare later
key = unpackSpec.key.substr(0, keysplitIndex + 1);
nlohmann::json j;
result =
details::unpackValue<nlohmann::json>(item.second, key, res, j) &&
result;
if (!result) {
return result;
}
std::vector<PerUnpack> nextLevel;
for (PerUnpack& p : toUnpack) {
if (!p.key.starts_with(key)) {
continue;
}
std::string_view thisLeftover = p.key.substr(key.size());
nextLevel.push_back({thisLeftover, p.value, false});
p.complete = true;
}
result = readJsonHelper(j, res, nextLevel) && result;
break;
}
result = std::visit(
[&item, &unpackSpec, &res](auto&& val) {
using ContainedT =
std::remove_pointer_t<std::decay_t<decltype(val)>>;
return details::unpackValue<ContainedT>(
item.second, unpackSpec.key, res, *val);
},
unpackSpec.value) &&
result;
unpackSpec.complete = true;
break;
}
if (unpackIndex == toUnpack.size()) {
messages::propertyUnknown(res, item.first);
result = false;
}
}
for (PerUnpack& perUnpack : toUnpack) {
if (!perUnpack.complete) {
bool isOptional = std::visit(
[](auto&& val) {
using ContainedType =
std::remove_pointer_t<std::decay_t<decltype(val)>>;
return details::IsOptional<ContainedType>::value;
},
perUnpack.value);
if (isOptional) {
continue;
}
messages::propertyMissing(res, perUnpack.key);
result = false;
}
}
return result;
}
inline void packVariant(std::span<PerUnpack> /*toPack*/) {}
template <typename FirstType, typename... UnpackTypes>
void packVariant(std::span<PerUnpack> toPack, std::string_view key,
FirstType& first, UnpackTypes&&... in) {
if (toPack.empty()) {
return;
}
toPack[0].key = key;
toPack[0].value = &first;
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
packVariant(toPack.subspan(1), std::forward<UnpackTypes&&>(in)...);
}
template <typename FirstType, typename... UnpackTypes>
bool readJson(nlohmann::json& jsonRequest, crow::Response& res,
std::string_view key, FirstType&& first, UnpackTypes&&... in) {
const std::size_t n = sizeof...(UnpackTypes) + 2;
std::array<PerUnpack, n / 2> toUnpack2;
packVariant(toUnpack2, key, first, std::forward<UnpackTypes&&>(in)...);
return readJsonHelper(jsonRequest, res, toUnpack2);
}
inline std::optional<nlohmann::json> readJsonPatchHelper(
const crow::Request& req, crow::Response& res) {
nlohmann::json jsonRequest;
if (!json_util::processJsonFromRequest(res, req, jsonRequest)) {
BMCWEB_LOG_DEBUG << "Json value not readable";
return std::nullopt;
}
nlohmann::json::object_t* object =
jsonRequest.get_ptr<nlohmann::json::object_t*>();
if (object == nullptr || object->empty()) {
BMCWEB_LOG_DEBUG << "Json value is empty";
messages::emptyJSON(res);
return std::nullopt;
}
std::erase_if(*object,
[](const std::pair<std::string, nlohmann::json>& item) {
return item.first.starts_with("@odata.");
});
if (object->empty()) {
// If the update request only contains OData annotations, the service
// should return the HTTP 400 Bad Request status code with the
// NoOperation message from the Base Message Registry, ...
messages::noOperation(res);
return std::nullopt;
}
return {std::move(jsonRequest)};
}
template <typename... UnpackTypes>
bool readJsonPatch(const crow::Request& req, crow::Response& res,
std::string_view key, UnpackTypes&&... in) {
std::optional<nlohmann::json> jsonRequest = readJsonPatchHelper(req, res);
if (jsonRequest == std::nullopt) {
return false;
}
return readJson(*jsonRequest, res, key, std::forward<UnpackTypes&&>(in)...);
}
/* readJsonAction
*
* returns false if argument parsing failed
*/
template <typename... UnpackTypes>
bool readJsonAction(const crow::Request& req, crow::Response& res,
const char* key, UnpackTypes&&... in) {
nlohmann::json jsonRequest;
if (!json_util::processJsonFromRequest(res, req, jsonRequest)) {
BMCWEB_LOG_DEBUG << "Json value not readable";
return false;
}
return readJson(jsonRequest, res, key, std::forward<UnpackTypes&&>(in)...);
}
// Sort the JSON array by |element[key]|.
// Each element shall contains |key| and the corresponding value shall be of
// type |nlohmann::json::string_t|; otherwise, returns false.
// For wehther sorting is performed, please refer to the following:
// if the all objects in the array either "have the key and the key value is
// string" or "the first key in the object has a value that is string", sorting
// will be performed, otherwise not. the array of mixed key and no key objects
// will be sorted as follows:
// 1. Elements without |key| are smaller than elements with |key|.
// 2. For elements without |key|, the order is based on first_key then
// |element[first_key]|.
// 3. For elements with same |key|, the order is based on
// |element[key]|.
// Altough we can safely assume that each element[key] is string based
// on current usage. In production usage of the function, each element will have
// the target key and the corresponding value is string based on current usage.
inline bool sortJsonArrayByKey(const std::string& key,
nlohmann::json::array_t& array) {
bool succeeded = false;
// only if all elements have the key and the key value is string, the sort is
// deemed successful.
if (std::all_of(array.begin(), array.end(), [&key](const nlohmann::json& a) {
auto it = a.find(key);
if (it == a.end()) {
return false;
}
const nlohmann::json::string_t* v =
it->get_ptr<const nlohmann::json::string_t*>();
if (v == nullptr) {
return false;
}
return true;
})) {
succeeded = true;
}
// If the array is a mixture of objects with and without the target key,
// sorting is performed only if the first key in the object without key or
// target key in object have a string value.
if (!std::all_of(array.begin(), array.end(), [&key](const nlohmann::json& a) {
auto it = a.find(key);
if (it != a.end()) {
return it->get_ptr<const nlohmann::json::string_t*>() != nullptr;
}
const nlohmann::json::string_t* v =
a.begin()->get_ptr<const nlohmann::json::string_t*>();
return v != nullptr;
})) {
// If the first key of the object doesn't have a string value, no
// sorting is performed.
return false;
}
std::sort(array.begin(), array.end(),
[comparator = AlphanumLess<nlohmann::json::string_t>(), &key](
const nlohmann::json& a, const nlohmann::json& b) mutable {
auto aIt = a.find(key);
auto bIt = b.find(key);
if (aIt == a.end() && bIt == b.end()) {
// If both arrays don't have the key, compare the first key of
// the object then value
auto akey = a.begin().key();
auto aValue = a.begin().value();
auto bkey = b.begin().key();
auto bValue = b.begin().value();
if (!comparator(akey, bkey) && !comparator(bkey, akey)) {
return comparator(aValue, bValue);
}
return comparator(akey, bkey);
}
// If one object doesn't have the key, the other one is larger.
if (aIt == a.end()) {
return true;
}
if (bIt == b.end()) {
return false;
}
// If both objects have the key, compare the value.
const nlohmann::json::string_t* nameA =
aIt->get_ptr<const nlohmann::json::string_t*>();
const nlohmann::json::string_t* nameB =
bIt->get_ptr<const nlohmann::json::string_t*>();
return comparator(*nameA, *nameB);
});
return succeeded;
}
} // namespace json_util
} // namespace redfish
#endif // THIRD_PARTY_GBMCWEB_REDFISH_CORE_INCLUDE_UTILS_JSON_UTILS_H_