1 /**
2  * Copyright © 2020 IBM 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 
17 #include "user_data_json.hpp"
18 
19 #include "json_utils.hpp"
20 #include "pel_types.hpp"
21 #include "pel_values.hpp"
22 #include "stream.hpp"
23 #include "user_data_formats.hpp"
24 
25 #include <Python.h>
26 
27 #include <iomanip>
28 #include <nlohmann/json.hpp>
29 #include <phosphor-logging/log.hpp>
30 #include <sstream>
31 
32 namespace openpower::pels::user_data
33 {
34 namespace pv = openpower::pels::pel_values;
35 using namespace phosphor::logging;
36 using orderedJSON = nlohmann::ordered_json;
37 
38 void pyDecRef(PyObject* pyObj)
39 {
40     Py_XDECREF(pyObj);
41 }
42 
43 /**
44  * @brief Returns a JSON string for use by PEL::printSectionInJSON().
45  *
46  * The returning string will contain a JSON object, but without
47  * the outer {}.  If the input JSON isn't a JSON object (dict), then
48  * one will be created with the input added to a 'Data' key.
49  *
50  * @param[in] json - The JSON to convert to a string
51  *
52  * @return std::string - The JSON string
53  */
54 std::string prettyJSON(uint16_t componentID, uint8_t subType, uint8_t version,
55                        const orderedJSON& json)
56 {
57     orderedJSON output;
58     output[pv::sectionVer] = std::to_string(version);
59     output[pv::subSection] = std::to_string(subType);
60     output[pv::createdBy] = getNumberString("0x%04X", componentID);
61 
62     if (!json.is_object())
63     {
64         output["Data"] = json;
65     }
66     else
67     {
68         for (const auto& [key, value] : json.items())
69         {
70             output[key] = value;
71         }
72     }
73 
74     // Let nlohmann do the pretty printing.
75     std::stringstream stream;
76     stream << std::setw(4) << output;
77 
78     auto jsonString = stream.str();
79 
80     // Now it looks like:
81     // {
82     //     "Section Version": ...
83     //     ...
84     // }
85 
86     // Since PEL::printSectionInJSON() will supply the outer { }s,
87     // remove the existing ones.
88 
89     // Replace the { and the following newline, and the } and its
90     // preceeding newline.
91     jsonString.erase(0, 2);
92 
93     auto pos = jsonString.find_last_of('}');
94     jsonString.erase(pos - 1);
95 
96     return jsonString;
97 }
98 
99 /**
100  * @brief Return a JSON string from the passed in CBOR data.
101  *
102  * @param[in] componentID - The comp ID from the UserData section header
103  * @param[in] subType - The subtype from the UserData section header
104  * @param[in] version - The version from the UserData section header
105  * @param[in] data - The CBOR data
106  *
107  * @return std::string - The JSON string
108  */
109 std::string getCBORJSON(uint16_t componentID, uint8_t subType, uint8_t version,
110                         const std::vector<uint8_t>& data)
111 {
112     // The CBOR parser needs the pad bytes added to 4 byte align
113     // removed.  The number of bytes added to the pad is on the
114     // very end, so will remove both fields before parsing.
115 
116     // If the data vector is too short, an exception will get
117     // thrown which will be handled up the call stack.
118 
119     auto cborData = data;
120     uint32_t pad{};
121 
122     Stream stream{cborData};
123     stream.offset(cborData.size() - 4);
124     stream >> pad;
125 
126     if (cborData.size() > (pad + sizeof(pad)))
127     {
128         cborData.resize(data.size() - sizeof(pad) - pad);
129     }
130 
131     orderedJSON json = nlohmann::json::from_cbor(cborData);
132 
133     return prettyJSON(componentID, subType, version, json);
134 }
135 
136 /**
137  * @brief Return a JSON string from the passed in text data.
138  *
139  * The function breaks up the input text into a vector of strings with
140  * newline as separator and converts that into JSON.  It will convert any
141  * unprintable characters to periods.
142  *
143  * @param[in] componentID - The comp ID from the UserData section header
144  * @param[in] subType - The subtype from the UserData section header
145  * @param[in] version - The version from the UserData section header
146  * @param[in] data - The CBOR data
147  *
148  * @return std::string - The JSON string
149  */
150 std::string getTextJSON(uint16_t componentID, uint8_t subType, uint8_t version,
151                         const std::vector<uint8_t>& data)
152 {
153     std::vector<std::string> text;
154     size_t startPos = 0;
155 
156     // Converts any unprintable characters to periods
157     auto validate = [](char& ch) {
158         if ((ch < ' ') || (ch > '~'))
159         {
160             ch = '.';
161         }
162     };
163 
164     // Break up the data into an array of strings with newline as separator
165     for (size_t pos = 0; pos < data.size(); ++pos)
166     {
167         if (data[pos] == '\n')
168         {
169             std::string line{reinterpret_cast<const char*>(&data[startPos]),
170                              pos - startPos};
171             std::for_each(line.begin(), line.end(), validate);
172             text.push_back(std::move(line));
173             startPos = pos + 1;
174         }
175     }
176     if (startPos < data.size())
177     {
178         std::string line{reinterpret_cast<const char*>(&data[startPos]),
179                          data.size() - startPos};
180         std::for_each(line.begin(), line.end(), validate);
181         text.push_back(std::move(line));
182     }
183 
184     orderedJSON json = text;
185     return prettyJSON(componentID, subType, version, json);
186 }
187 
188 /**
189  * @brief Convert to an appropriate JSON string as the data is one of
190  *        the formats that we natively support.
191  *
192  * @param[in] componentID - The comp ID from the UserData section header
193  * @param[in] subType - The subtype from the UserData section header
194  * @param[in] version - The version from the UserData section header
195  * @param[in] data - The data itself
196  *
197  * @return std::optional<std::string> - The JSON string if it could be created,
198  *                                      else std::nullopt.
199  */
200 std::optional<std::string>
201     getBuiltinFormatJSON(uint16_t componentID, uint8_t subType, uint8_t version,
202                          const std::vector<uint8_t>& data)
203 {
204     switch (subType)
205     {
206         case static_cast<uint8_t>(UserDataFormat::json):
207         {
208             std::string jsonString{data.begin(), data.begin() + data.size()};
209 
210             orderedJSON json = nlohmann::json::parse(jsonString);
211 
212             return prettyJSON(componentID, subType, version, json);
213         }
214         case static_cast<uint8_t>(UserDataFormat::cbor):
215         {
216             return getCBORJSON(componentID, subType, version, data);
217         }
218         case static_cast<uint8_t>(UserDataFormat::text):
219         {
220             return getTextJSON(componentID, subType, version, data);
221         }
222         default:
223             break;
224     }
225     return std::nullopt;
226 }
227 
228 /**
229  * @brief Call Python modules to parse the data into a JSON string
230  *
231  * The module to call is based on the Creator Subsystem ID and the Component
232  * ID under the namespace "udparsers". For example: "udparsers.xyyyy.xyyyy"
233  * where "x" is the Creator Subsystem ID and "yyyy" is the Component ID.
234  *
235  * All modules must provide the following:
236  * Function: parseUDToJson
237  * Argument list:
238  *    1. (int) Sub-section type
239  *    2. (int) Section version
240  *    3. (memoryview): Data
241  *-Return data:
242  *    1. (str) JSON string
243  *
244  * @param[in] componentID - The comp ID from the UserData section header
245  * @param[in] subType - The subtype from the UserData section header
246  * @param[in] version - The version from the UserData section header
247  * @param[in] data - The data itself
248  * @param[in] creatorID - The creatorID from the PrivateHeader section
249  * @return std::optional<std::string> - The JSON string if it could be created,
250  *                                      else std::nullopt
251  */
252 std::optional<std::string> getPythonJSON(uint16_t componentID, uint8_t subType,
253                                          uint8_t version,
254                                          const std::vector<uint8_t>& data,
255                                          uint8_t creatorID)
256 {
257     PyObject *pName, *pModule, *pDict, *pFunc, *pArgs, *pData, *pResult,
258         *pBytes, *eType, *eValue, *eTraceback;
259     std::string pErrStr;
260     std::string module = getNumberString("%c", tolower(creatorID)) +
261                          getNumberString("%04x", componentID);
262     pName = PyUnicode_FromString(
263         std::string("udparsers." + module + "." + module).c_str());
264     std::unique_ptr<PyObject, decltype(&pyDecRef)> modNamePtr(pName, &pyDecRef);
265     pModule = PyImport_Import(pName);
266     std::unique_ptr<PyObject, decltype(&pyDecRef)> modPtr(pModule, &pyDecRef);
267     if (pModule == NULL)
268     {
269         pErrStr = "No error string found";
270         PyErr_Fetch(&eType, &eValue, &eTraceback);
271         if (eValue)
272         {
273             PyObject* pStr = PyObject_Str(eValue);
274             if (pStr)
275             {
276                 pErrStr = PyUnicode_AsUTF8(pStr);
277             }
278             Py_XDECREF(pStr);
279         }
280     }
281     else
282     {
283         pDict = PyModule_GetDict(pModule);
284         pFunc = PyDict_GetItemString(pDict, "parseUDToJson");
285         if (PyCallable_Check(pFunc))
286         {
287             auto ud = data.data();
288             pArgs = PyTuple_New(3);
289             std::unique_ptr<PyObject, decltype(&pyDecRef)> argPtr(pArgs,
290                                                                   &pyDecRef);
291             PyTuple_SetItem(pArgs, 0,
292                             PyLong_FromUnsignedLong((unsigned long)subType));
293             PyTuple_SetItem(pArgs, 1,
294                             PyLong_FromUnsignedLong((unsigned long)version));
295             pData = PyMemoryView_FromMemory(
296                 reinterpret_cast<char*>(const_cast<unsigned char*>(ud)),
297                 data.size(), PyBUF_READ);
298             std::unique_ptr<PyObject, decltype(&pyDecRef)> dataPtr(pData,
299                                                                    &pyDecRef);
300             PyTuple_SetItem(pArgs, 2, pData);
301             pResult = PyObject_CallObject(pFunc, pArgs);
302             std::unique_ptr<PyObject, decltype(&pyDecRef)> resPtr(pResult,
303                                                                   &pyDecRef);
304             if (pResult)
305             {
306                 pBytes = PyUnicode_AsEncodedString(pResult, "utf-8", "~E~");
307                 std::unique_ptr<PyObject, decltype(&pyDecRef)> pyBytePtr(
308                     pBytes, &pyDecRef);
309                 const char* output = PyBytes_AS_STRING(pBytes);
310                 try
311                 {
312                     orderedJSON json = nlohmann::json::parse(output);
313                     return prettyJSON(componentID, subType, version, json);
314                 }
315                 catch (std::exception& e)
316                 {
317                     log<level::ERR>("Bad JSON from parser",
318                                     entry("ERROR=%s", e.what()),
319                                     entry("PARSER_MODULE=%s", module.c_str()),
320                                     entry("SUBTYPE=0x%X", subType),
321                                     entry("VERSION=%d", version),
322                                     entry("DATA_LENGTH=%lu\n", data.size()));
323                     return std::nullopt;
324                 }
325             }
326             else
327             {
328                 pErrStr = "No error string found";
329                 PyErr_Fetch(&eType, &eValue, &eTraceback);
330                 if (eValue)
331                 {
332                     PyObject* pStr = PyObject_Str(eValue);
333                     if (pStr)
334                     {
335                         pErrStr = PyUnicode_AsUTF8(pStr);
336                     }
337                     Py_XDECREF(pStr);
338                 }
339             }
340         }
341     }
342     if (!pErrStr.empty())
343     {
344         log<level::ERR>("Python exception thrown by parser",
345                         entry("ERROR=%s", pErrStr.c_str()),
346                         entry("PARSER_MODULE=%s", module.c_str()),
347                         entry("SUBTYPE=0x%X", subType),
348                         entry("VERSION=%d", version),
349                         entry("DATA_LENGTH=%lu\n", data.size()));
350     }
351     Py_XDECREF(eType);
352     Py_XDECREF(eValue);
353     Py_XDECREF(eTraceback);
354     return std::nullopt;
355 }
356 
357 std::optional<std::string> getJSON(uint16_t componentID, uint8_t subType,
358                                    uint8_t version,
359                                    const std::vector<uint8_t>& data,
360                                    uint8_t creatorID,
361                                    const std::vector<std::string>& plugins)
362 {
363     std::string subsystem = getNumberString("%c", tolower(creatorID));
364     std::string component = getNumberString("%04x", componentID);
365     try
366     {
367         if (pv::creatorIDs.at(getNumberString("%c", creatorID)) == "BMC" &&
368             componentID == static_cast<uint16_t>(ComponentID::phosphorLogging))
369         {
370             return getBuiltinFormatJSON(componentID, subType, version, data);
371         }
372         else if (std::find(plugins.begin(), plugins.end(),
373                            subsystem + component) != plugins.end())
374         {
375             return getPythonJSON(componentID, subType, version, data,
376                                  creatorID);
377         }
378     }
379     catch (std::exception& e)
380     {
381         log<level::ERR>("Failed parsing UserData", entry("ERROR=%s", e.what()),
382                         entry("COMP_ID=0x%X", componentID),
383                         entry("SUBTYPE=0x%X", subType),
384                         entry("VERSION=%d", version),
385                         entry("DATA_LENGTH=%lu\n", data.size()));
386     }
387 
388     return std::nullopt;
389 }
390 
391 } // namespace openpower::pels::user_data
392