xref: /openbmc/bmcweb/redfish-core/include/utils/json_utils.hpp (revision 5be2b14a2553f2aa0b64d1f88494b0561739d964)
1 /*
2 // Copyright (c) 2018 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 #pragma once
17 
18 #include "error_messages.hpp"
19 #include "http_connection.hpp"
20 #include "http_request.hpp"
21 #include "http_response.hpp"
22 #include "human_sort.hpp"
23 #include "logging.hpp"
24 
25 #include <nlohmann/json.hpp>
26 
27 #include <algorithm>
28 #include <array>
29 #include <cmath>
30 #include <cstddef>
31 #include <cstdint>
32 #include <limits>
33 #include <map>
34 #include <optional>
35 #include <ranges>
36 #include <span>
37 #include <string>
38 #include <string_view>
39 #include <type_traits>
40 #include <utility>
41 #include <variant>
42 #include <vector>
43 
44 // IWYU pragma: no_include <stdint.h>
45 // IWYU pragma: no_forward_declare crow::Request
46 
47 namespace redfish
48 {
49 
50 namespace json_util
51 {
52 
53 /**
54  * @brief Processes request to extract JSON from its body. If it fails, adds
55  *       MalformedJSON message to response and ends it.
56  *
57  * @param[io]  res       Response object
58  * @param[in]  req       Request object
59  * @param[out] reqJson   JSON object extracted from request's body
60  *
61  * @return true if JSON is valid, false when JSON is invalid and response has
62  *         been filled with message and ended.
63  */
64 bool processJsonFromRequest(crow::Response& res, const crow::Request& req,
65                             nlohmann::json& reqJson);
66 namespace details
67 {
68 
69 template <typename Type>
70 struct IsOptional : std::false_type
71 {};
72 
73 template <typename Type>
74 struct IsOptional<std::optional<Type>> : std::true_type
75 {};
76 
77 template <typename Type>
78 struct IsVector : std::false_type
79 {};
80 
81 template <typename Type>
82 struct IsVector<std::vector<Type>> : std::true_type
83 {};
84 
85 template <typename Type>
86 struct IsStdArray : std::false_type
87 {};
88 
89 template <typename Type, std::size_t size>
90 struct IsStdArray<std::array<Type, size>> : std::true_type
91 {};
92 
93 enum class UnpackErrorCode
94 {
95     success,
96     invalidType,
97     outOfRange
98 };
99 
100 template <typename ToType, typename FromType>
101 bool checkRange(const FromType& from, std::string_view key)
102 {
103     if (from > std::numeric_limits<ToType>::max())
104     {
105         BMCWEB_LOG_DEBUG("Value for key {} was greater than max: {}", key,
106                          __PRETTY_FUNCTION__);
107         return false;
108     }
109     if (from < std::numeric_limits<ToType>::lowest())
110     {
111         BMCWEB_LOG_DEBUG("Value for key {} was less than min: {}", key,
112                          __PRETTY_FUNCTION__);
113         return false;
114     }
115     if constexpr (std::is_floating_point_v<ToType>)
116     {
117         if (std::isnan(from))
118         {
119             BMCWEB_LOG_DEBUG("Value for key {} was NAN", key);
120             return false;
121         }
122     }
123 
124     return true;
125 }
126 
127 template <typename Type>
128 UnpackErrorCode unpackValueWithErrorCode(nlohmann::json& jsonValue,
129                                          std::string_view key, Type& value)
130 {
131     UnpackErrorCode ret = UnpackErrorCode::success;
132 
133     if constexpr (std::is_floating_point_v<Type>)
134     {
135         double helper = 0;
136         double* jsonPtr = jsonValue.get_ptr<double*>();
137 
138         if (jsonPtr == nullptr)
139         {
140             int64_t* intPtr = jsonValue.get_ptr<int64_t*>();
141             if (intPtr != nullptr)
142             {
143                 helper = static_cast<double>(*intPtr);
144                 jsonPtr = &helper;
145             }
146         }
147         if (jsonPtr == nullptr)
148         {
149             return UnpackErrorCode::invalidType;
150         }
151         if (!checkRange<Type>(*jsonPtr, key))
152         {
153             return UnpackErrorCode::outOfRange;
154         }
155         value = static_cast<Type>(*jsonPtr);
156     }
157 
158     else if constexpr (std::is_signed_v<Type>)
159     {
160         int64_t* jsonPtr = jsonValue.get_ptr<int64_t*>();
161         if (jsonPtr == nullptr)
162         {
163             return UnpackErrorCode::invalidType;
164         }
165         if (!checkRange<Type>(*jsonPtr, key))
166         {
167             return UnpackErrorCode::outOfRange;
168         }
169         value = static_cast<Type>(*jsonPtr);
170     }
171 
172     else if constexpr ((std::is_unsigned_v<Type>)&&(
173                            !std::is_same_v<bool, Type>))
174     {
175         uint64_t* jsonPtr = jsonValue.get_ptr<uint64_t*>();
176         if (jsonPtr == nullptr)
177         {
178             return UnpackErrorCode::invalidType;
179         }
180         if (!checkRange<Type>(*jsonPtr, key))
181         {
182             return UnpackErrorCode::outOfRange;
183         }
184         value = static_cast<Type>(*jsonPtr);
185     }
186 
187     else if constexpr (std::is_same_v<nlohmann::json, Type>)
188     {
189         value = std::move(jsonValue);
190     }
191     else
192     {
193         using JsonType = std::add_const_t<std::add_pointer_t<Type>>;
194         JsonType jsonPtr = jsonValue.get_ptr<JsonType>();
195         if (jsonPtr == nullptr)
196         {
197             BMCWEB_LOG_DEBUG("Value for key {} was incorrect type: {}", key,
198                              jsonValue.type_name());
199             return UnpackErrorCode::invalidType;
200         }
201         value = std::move(*jsonPtr);
202     }
203     return ret;
204 }
205 
206 template <typename Type>
207 bool unpackValue(nlohmann::json& jsonValue, std::string_view key,
208                  crow::Response& res, Type& value)
209 {
210     bool ret = true;
211 
212     if constexpr (IsOptional<Type>::value)
213     {
214         value.emplace();
215         ret = unpackValue<typename Type::value_type>(jsonValue, key, res,
216                                                      *value) &&
217               ret;
218     }
219     else if constexpr (IsStdArray<Type>::value)
220     {
221         if (!jsonValue.is_array())
222         {
223             messages::propertyValueTypeError(res, res.jsonValue, key);
224             return false;
225         }
226         if (jsonValue.size() != value.size())
227         {
228             messages::propertyValueTypeError(res, res.jsonValue, key);
229             return false;
230         }
231         size_t index = 0;
232         for (const auto& val : jsonValue.items())
233         {
234             ret = unpackValue<typename Type::value_type>(val.value(), key, res,
235                                                          value[index++]) &&
236                   ret;
237         }
238     }
239     else if constexpr (IsVector<Type>::value)
240     {
241         if (!jsonValue.is_array())
242         {
243             messages::propertyValueTypeError(res, res.jsonValue, key);
244             return false;
245         }
246 
247         for (const auto& val : jsonValue.items())
248         {
249             value.emplace_back();
250             ret = unpackValue<typename Type::value_type>(val.value(), key, res,
251                                                          value.back()) &&
252                   ret;
253         }
254     }
255     else
256     {
257         UnpackErrorCode ec = unpackValueWithErrorCode(jsonValue, key, value);
258         if (ec != UnpackErrorCode::success)
259         {
260             if (ec == UnpackErrorCode::invalidType)
261             {
262                 messages::propertyValueTypeError(res, jsonValue, key);
263             }
264             else if (ec == UnpackErrorCode::outOfRange)
265             {
266                 messages::propertyValueNotInList(res, jsonValue, key);
267             }
268             return false;
269         }
270     }
271 
272     return ret;
273 }
274 
275 template <typename Type>
276 bool unpackValue(nlohmann::json& jsonValue, std::string_view key, Type& value)
277 {
278     bool ret = true;
279     if constexpr (IsOptional<Type>::value)
280     {
281         value.emplace();
282         ret = unpackValue<typename Type::value_type>(jsonValue, key, *value) &&
283               ret;
284     }
285     else if constexpr (IsStdArray<Type>::value)
286     {
287         if (!jsonValue.is_array())
288         {
289             return false;
290         }
291         if (jsonValue.size() != value.size())
292         {
293             return false;
294         }
295         size_t index = 0;
296         for (const auto& val : jsonValue.items())
297         {
298             ret = unpackValue<typename Type::value_type>(val.value(), key,
299                                                          value[index++]) &&
300                   ret;
301         }
302     }
303     else if constexpr (IsVector<Type>::value)
304     {
305         if (!jsonValue.is_array())
306         {
307             return false;
308         }
309 
310         for (const auto& val : jsonValue.items())
311         {
312             value.emplace_back();
313             ret = unpackValue<typename Type::value_type>(val.value(), key,
314                                                          value.back()) &&
315                   ret;
316         }
317     }
318     else
319     {
320         UnpackErrorCode ec = unpackValueWithErrorCode(jsonValue, key, value);
321         if (ec != UnpackErrorCode::success)
322         {
323             return false;
324         }
325     }
326 
327     return ret;
328 }
329 } // namespace details
330 
331 // clang-format off
332 using UnpackVariant = std::variant<
333     uint8_t*,
334     uint16_t*,
335     int16_t*,
336     uint32_t*,
337     int32_t*,
338     uint64_t*,
339     int64_t*,
340     bool*,
341     double*,
342     std::string*,
343     nlohmann::json*,
344     nlohmann::json::object_t*,
345     std::vector<uint8_t>*,
346     std::vector<uint16_t>*,
347     std::vector<int16_t>*,
348     std::vector<uint32_t>*,
349     std::vector<int32_t>*,
350     std::vector<uint64_t>*,
351     std::vector<int64_t>*,
352     //std::vector<bool>*,
353     std::vector<double>*,
354     std::vector<std::string>*,
355     std::vector<nlohmann::json>*,
356     std::vector<nlohmann::json::object_t>*,
357     std::optional<uint8_t>*,
358     std::optional<uint16_t>*,
359     std::optional<int16_t>*,
360     std::optional<uint32_t>*,
361     std::optional<int32_t>*,
362     std::optional<uint64_t>*,
363     std::optional<int64_t>*,
364     std::optional<bool>*,
365     std::optional<double>*,
366     std::optional<std::string>*,
367     std::optional<nlohmann::json>*,
368     std::optional<nlohmann::json::object_t>*,
369     std::optional<std::vector<uint8_t>>*,
370     std::optional<std::vector<uint16_t>>*,
371     std::optional<std::vector<int16_t>>*,
372     std::optional<std::vector<uint32_t>>*,
373     std::optional<std::vector<int32_t>>*,
374     std::optional<std::vector<uint64_t>>*,
375     std::optional<std::vector<int64_t>>*,
376     //std::optional<std::vector<bool>>*,
377     std::optional<std::vector<double>>*,
378     std::optional<std::vector<std::string>>*,
379     std::optional<std::vector<nlohmann::json>>*,
380     std::optional<std::vector<nlohmann::json::object_t>>*
381 >;
382 // clang-format on
383 
384 struct PerUnpack
385 {
386     std::string_view key;
387     UnpackVariant value;
388     bool complete = false;
389 };
390 
391 inline bool readJsonHelper(nlohmann::json& jsonRequest, crow::Response& res,
392                            std::span<PerUnpack> toUnpack);
393 
394 inline bool readJsonHelperObject(nlohmann::json::object_t& obj,
395                                  crow::Response& res,
396                                  std::span<PerUnpack> toUnpack)
397 {
398     bool result = true;
399     for (auto& item : obj)
400     {
401         size_t unpackIndex = 0;
402         for (; unpackIndex < toUnpack.size(); unpackIndex++)
403         {
404             PerUnpack& unpackSpec = toUnpack[unpackIndex];
405             std::string_view key = unpackSpec.key;
406             size_t keysplitIndex = key.find('/');
407             std::string_view leftover;
408             if (keysplitIndex != std::string_view::npos)
409             {
410                 leftover = key.substr(keysplitIndex + 1);
411                 key = key.substr(0, keysplitIndex);
412             }
413 
414             if (key != item.first || unpackSpec.complete)
415             {
416                 continue;
417             }
418 
419             // Sublevel key
420             if (!leftover.empty())
421             {
422                 // Include the slash in the key so we can compare later
423                 key = unpackSpec.key.substr(0, keysplitIndex + 1);
424                 nlohmann::json j;
425                 result = details::unpackValue<nlohmann::json>(item.second, key,
426                                                               res, j) &&
427                          result;
428                 if (!result)
429                 {
430                     return result;
431                 }
432 
433                 std::vector<PerUnpack> nextLevel;
434                 for (PerUnpack& p : toUnpack)
435                 {
436                     if (!p.key.starts_with(key))
437                     {
438                         continue;
439                     }
440                     std::string_view thisLeftover = p.key.substr(key.size());
441                     nextLevel.push_back({thisLeftover, p.value, false});
442                     p.complete = true;
443                 }
444 
445                 result = readJsonHelper(j, res, nextLevel) && result;
446                 break;
447             }
448 
449             result = std::visit(
450                          [&item, &unpackSpec, &res](auto&& val) {
451                 using ContainedT =
452                     std::remove_pointer_t<std::decay_t<decltype(val)>>;
453                 return details::unpackValue<ContainedT>(
454                     item.second, unpackSpec.key, res, *val);
455             },
456                          unpackSpec.value) &&
457                      result;
458 
459             unpackSpec.complete = true;
460             break;
461         }
462 
463         if (unpackIndex == toUnpack.size())
464         {
465             messages::propertyUnknown(res, item.first);
466             result = false;
467         }
468     }
469 
470     for (PerUnpack& perUnpack : toUnpack)
471     {
472         if (!perUnpack.complete)
473         {
474             bool isOptional = std::visit(
475                 [](auto&& val) {
476                 using ContainedType =
477                     std::remove_pointer_t<std::decay_t<decltype(val)>>;
478                 return details::IsOptional<ContainedType>::value;
479             },
480                 perUnpack.value);
481             if (isOptional)
482             {
483                 continue;
484             }
485             messages::propertyMissing(res, perUnpack.key);
486             result = false;
487         }
488     }
489     return result;
490 }
491 
492 inline bool readJsonHelper(nlohmann::json& jsonRequest, crow::Response& res,
493                            std::span<PerUnpack> toUnpack)
494 {
495     nlohmann::json::object_t* obj =
496         jsonRequest.get_ptr<nlohmann::json::object_t*>();
497     if (obj == nullptr)
498     {
499         BMCWEB_LOG_DEBUG("Json value is not an object");
500         messages::unrecognizedRequestBody(res);
501         return false;
502     }
503     return readJsonHelperObject(*obj, res, toUnpack);
504 }
505 
506 inline void packVariant(std::span<PerUnpack> /*toPack*/) {}
507 
508 template <typename FirstType, typename... UnpackTypes>
509 void packVariant(std::span<PerUnpack> toPack, std::string_view key,
510                  FirstType& first, UnpackTypes&&... in)
511 {
512     if (toPack.empty())
513     {
514         return;
515     }
516     toPack[0].key = key;
517     toPack[0].value = &first;
518     // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
519     packVariant(toPack.subspan(1), std::forward<UnpackTypes&&>(in)...);
520 }
521 
522 template <typename FirstType, typename... UnpackTypes>
523 bool readJsonObject(nlohmann::json::object_t& jsonRequest, crow::Response& res,
524                     std::string_view key, FirstType&& first,
525                     UnpackTypes&&... in)
526 {
527     const std::size_t n = sizeof...(UnpackTypes) + 2;
528     std::array<PerUnpack, n / 2> toUnpack2;
529     packVariant(toUnpack2, key, first, std::forward<UnpackTypes&&>(in)...);
530     return readJsonHelperObject(jsonRequest, res, toUnpack2);
531 }
532 
533 template <typename FirstType, typename... UnpackTypes>
534 bool readJson(nlohmann::json& jsonRequest, crow::Response& res,
535               std::string_view key, FirstType&& first, UnpackTypes&&... in)
536 {
537     nlohmann::json::object_t* obj =
538         jsonRequest.get_ptr<nlohmann::json::object_t*>();
539     if (obj == nullptr)
540     {
541         BMCWEB_LOG_DEBUG("Json value is not an object");
542         messages::unrecognizedRequestBody(res);
543         return false;
544     }
545     return readJsonObject(*obj, res, key, std::forward<FirstType>(first),
546                           std::forward<UnpackTypes&&>(in)...);
547 }
548 
549 inline std::optional<nlohmann::json::object_t>
550     readJsonPatchHelper(const crow::Request& req, crow::Response& res)
551 {
552     nlohmann::json jsonRequest;
553     if (!json_util::processJsonFromRequest(res, req, jsonRequest))
554     {
555         BMCWEB_LOG_DEBUG("Json value not readable");
556         return std::nullopt;
557     }
558     nlohmann::json::object_t* object =
559         jsonRequest.get_ptr<nlohmann::json::object_t*>();
560     if (object == nullptr || object->empty())
561     {
562         BMCWEB_LOG_DEBUG("Json value is empty");
563         messages::emptyJSON(res);
564         return std::nullopt;
565     }
566     std::erase_if(*object,
567                   [](const std::pair<std::string, nlohmann::json>& item) {
568         return item.first.starts_with("@odata.");
569     });
570     if (object->empty())
571     {
572         //  If the update request only contains OData annotations, the service
573         //  should return the HTTP 400 Bad Request status code with the
574         //  NoOperation message from the Base Message Registry, ...
575         messages::noOperation(res);
576         return std::nullopt;
577     }
578 
579     return {std::move(*object)};
580 }
581 
582 template <typename... UnpackTypes>
583 bool readJsonPatch(const crow::Request& req, crow::Response& res,
584                    std::string_view key, UnpackTypes&&... in)
585 {
586     std::optional<nlohmann::json> jsonRequest = readJsonPatchHelper(req, res);
587     if (!jsonRequest)
588     {
589         return false;
590     }
591     nlohmann::json::object_t* object =
592         jsonRequest->get_ptr<nlohmann::json::object_t*>();
593     if (object == nullptr)
594     {
595         BMCWEB_LOG_DEBUG("Json value is empty");
596         messages::emptyJSON(res);
597         return false;
598     }
599 
600     return readJsonObject(*object, res, key,
601                           std::forward<UnpackTypes&&>(in)...);
602 }
603 
604 template <typename... UnpackTypes>
605 bool readJsonAction(const crow::Request& req, crow::Response& res,
606                     const char* key, UnpackTypes&&... in)
607 {
608     nlohmann::json jsonRequest;
609     if (!json_util::processJsonFromRequest(res, req, jsonRequest))
610     {
611         BMCWEB_LOG_DEBUG("Json value not readable");
612         return false;
613     }
614     nlohmann::json::object_t* object =
615         jsonRequest.get_ptr<nlohmann::json::object_t*>();
616     if (object == nullptr)
617     {
618         BMCWEB_LOG_DEBUG("Json value is empty");
619         messages::emptyJSON(res);
620         return false;
621     }
622     return readJsonObject(*object, res, key,
623                           std::forward<UnpackTypes&&>(in)...);
624 }
625 
626 // Determines if two json objects are less, based on the presence of the
627 // @odata.id key
628 inline int odataObjectCmp(const nlohmann::json& a, const nlohmann::json& b)
629 {
630     using object_t = nlohmann::json::object_t;
631     const object_t* aObj = a.get_ptr<const object_t*>();
632     const object_t* bObj = b.get_ptr<const object_t*>();
633 
634     if (aObj == nullptr)
635     {
636         if (bObj == nullptr)
637         {
638             return 0;
639         }
640         return -1;
641     }
642     if (bObj == nullptr)
643     {
644         return 1;
645     }
646     object_t::const_iterator aIt = aObj->find("@odata.id");
647     object_t::const_iterator bIt = bObj->find("@odata.id");
648     // If either object doesn't have the key, they get "sorted" to the end.
649     if (aIt == aObj->end())
650     {
651         if (bIt == bObj->end())
652         {
653             return 0;
654         }
655         return -1;
656     }
657     if (bIt == bObj->end())
658     {
659         return 1;
660     }
661     const nlohmann::json::string_t* nameA =
662         aIt->second.get_ptr<const std::string*>();
663     const nlohmann::json::string_t* nameB =
664         bIt->second.get_ptr<const std::string*>();
665     // If either object doesn't have a string as the key, they get "sorted" to
666     // the end.
667     if (nameA == nullptr)
668     {
669         if (nameB == nullptr)
670         {
671             return 0;
672         }
673         return -1;
674     }
675     if (nameB == nullptr)
676     {
677         return 1;
678     }
679     boost::urls::url_view aUrl(*nameA);
680     boost::urls::url_view bUrl(*nameB);
681     auto segmentsAIt = aUrl.segments().begin();
682     auto segmentsBIt = bUrl.segments().begin();
683 
684     while (true)
685     {
686         if (segmentsAIt == aUrl.segments().end())
687         {
688             if (segmentsBIt == bUrl.segments().end())
689             {
690                 return 0;
691             }
692             return -1;
693         }
694         if (segmentsBIt == bUrl.segments().end())
695         {
696             return 1;
697         }
698         int res = alphanumComp(*segmentsAIt, *segmentsBIt);
699         if (res != 0)
700         {
701             return res;
702         }
703 
704         segmentsAIt++;
705         segmentsBIt++;
706     }
707 };
708 
709 struct ODataObjectLess
710 {
711     bool operator()(const nlohmann::json& left,
712                     const nlohmann::json& right) const
713     {
714         return odataObjectCmp(left, right) < 0;
715     }
716 };
717 
718 // Sort the JSON array by |element[key]|.
719 // Elements without |key| or type of |element[key]| is not string are smaller
720 // those whose |element[key]| is string.
721 inline void sortJsonArrayByOData(nlohmann::json::array_t& array)
722 {
723     std::ranges::sort(array, ODataObjectLess());
724 }
725 
726 // Returns the estimated size of the JSON value
727 // The implementation walks through every key and every value, accumulates the
728 //  total size of keys and values.
729 // Ideally, we should use a custom allocator that nlohmann JSON supports.
730 
731 // Assumption made:
732 //  1. number: 8 characters
733 //  2. boolean: 5 characters (False)
734 //  3. string: len(str) + 2 characters (quote)
735 //  4. bytes: len(bytes) characters
736 //  5. null: 4 characters (null)
737 uint64_t getEstimatedJsonSize(const nlohmann::json& root);
738 
739 } // namespace json_util
740 } // namespace redfish
741