blob: 43da0a363fd7e3a37b7b32e9858bd432de776f3e [file] [log] [blame]
/*
// 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.
*/
#pragma once
#include "error_messages.hpp"
#include "http_request.hpp"
#include "http_response.hpp"
#include "human_sort.hpp"
#include "logging.hpp"
#include "nlohmann/json.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <map>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
// 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.
// Elements without |key| or type of |element[key]| is not string are smaller
// those whose |element[key]| is string.
inline bool sortJsonArrayByKey(const std::string& key,
nlohmann::json::array_t& array)
{
bool succeeded = true;
std::sort(array.begin(), array.end(),
[comparator = AlphanumLess<nlohmann::json::string_t>(),
&succeeded,
&key](const nlohmann::json& a, const nlohmann::json& b) mutable {
auto aIt = a.find(key);
auto bIt = b.find(key);
if (aIt == a.end())
{
succeeded = false;
return true;
}
if (bIt == b.end())
{
succeeded = false;
return false;
}
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*>();
if (nameA == nullptr)
{
succeeded = false;
return true;
}
if (nameB == nullptr)
{
succeeded = false;
return false;
}
return comparator(*nameA, *nameB);
});
return succeeded;
}
} // namespace json_util
} // namespace redfish