xref: /openbmc/entity-manager/src/fru_device/fru_utils.cpp (revision 10c57656f31ed1c20778425b6090cfae7d788ab6)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
3 
4 #include "fru_utils.hpp"
5 
6 #include "gzip_utils.hpp"
7 
8 #include <phosphor-logging/lg2.hpp>
9 
10 #include <array>
11 #include <cstddef>
12 #include <cstdint>
13 #include <filesystem>
14 #include <iomanip>
15 #include <numeric>
16 #include <set>
17 #include <sstream>
18 #include <string>
19 #include <vector>
20 
21 extern "C"
22 {
23 // Include for I2C_SMBUS_BLOCK_MAX
24 #include <linux/i2c.h>
25 }
26 
27 constexpr size_t fruVersion = 1; // Current FRU spec version number is 1
28 
intelEpoch()29 std::tm intelEpoch()
30 {
31     std::tm val = {};
32     val.tm_year = 1996 - 1900;
33     val.tm_mday = 1;
34     return val;
35 }
36 
sixBitToChar(uint8_t val)37 char sixBitToChar(uint8_t val)
38 {
39     return static_cast<char>((val & 0x3f) + ' ');
40 }
41 
bcdPlusToChar(uint8_t val)42 char bcdPlusToChar(uint8_t val)
43 {
44     val &= 0xf;
45     return (val < 10) ? static_cast<char>(val + '0') : bcdHighChars[val - 10];
46 }
47 
48 enum FRUDataEncoding
49 {
50     binary = 0x0,
51     bcdPlus = 0x1,
52     sixBitASCII = 0x2,
53     languageDependent = 0x3,
54 };
55 
56 enum MultiRecordType : uint8_t
57 {
58     powerSupplyInfo = 0x00,
59     dcOutput = 0x01,
60     dcLoad = 0x02,
61     managementAccessRecord = 0x03,
62     baseCompatibilityRecord = 0x04,
63     extendedCompatibilityRecord = 0x05,
64     resvASFSMBusDeviceRecord = 0x06,
65     resvASFLegacyDeviceAlerts = 0x07,
66     resvASFRemoteControl = 0x08,
67     extendedDCOutput = 0x09,
68     extendedDCLoad = 0x0A
69 };
70 
71 enum SubManagementAccessRecord : uint8_t
72 {
73     systemManagementURL = 0x01,
74     systemName = 0x02,
75     systemPingAddress = 0x03,
76     componentManagementURL = 0x04,
77     componentName = 0x05,
78     componentPingAddress = 0x06,
79     systemUniqueID = 0x07
80 };
81 
82 /* Decode FRU data into a std::string, given an input iterator and end. If the
83  * state returned is fruDataOk, then the resulting string is the decoded FRU
84  * data. The input iterator is advanced past the data consumed.
85  *
86  * On fruDataErr, we have lost synchronisation with the length bytes, so the
87  * iterator is no longer usable.
88  */
decodeFRUData(std::span<const uint8_t>::const_iterator & iter,std::span<const uint8_t>::const_iterator & end,bool isLangEng)89 std::pair<DecodeState, std::string> decodeFRUData(
90     std::span<const uint8_t>::const_iterator& iter,
91     std::span<const uint8_t>::const_iterator& end, bool isLangEng)
92 {
93     std::string value;
94     unsigned int i = 0;
95 
96     /* we need at least one byte to decode the type/len header */
97     if (iter == end)
98     {
99         lg2::error("Truncated FRU data");
100         return make_pair(DecodeState::err, value);
101     }
102 
103     uint8_t c = *(iter++);
104 
105     /* 0xc1 is the end marker */
106     if (c == 0xc1)
107     {
108         return make_pair(DecodeState::end, value);
109     }
110 
111     /* decode type/len byte */
112     uint8_t type = static_cast<uint8_t>(c >> 6);
113     uint8_t len = static_cast<uint8_t>(c & 0x3f);
114 
115     /* we should have at least len bytes of data available overall */
116     if (iter + len > end)
117     {
118         lg2::error("FRU data field extends past end of FRU area data");
119         return make_pair(DecodeState::err, value);
120     }
121 
122     switch (type)
123     {
124         case FRUDataEncoding::binary:
125         {
126             std::stringstream ss;
127             ss << std::hex << std::setfill('0');
128             for (i = 0; i < len; i++, iter++)
129             {
130                 uint8_t val = static_cast<uint8_t>(*iter);
131                 ss << std::setw(2) << static_cast<int>(val);
132             }
133             value = ss.str();
134             break;
135         }
136         case FRUDataEncoding::languageDependent:
137             /* For language-code dependent encodings, assume 8-bit ASCII */
138             value = std::string(iter, iter + len);
139             iter += len;
140 
141             /* English text is encoded in 8-bit ASCII + Latin 1. All other
142              * languages are required to use 2-byte unicode. FruDevice does not
143              * handle unicode.
144              */
145             if (!isLangEng)
146             {
147                 lg2::error("Error: Non english string is not supported ");
148                 return make_pair(DecodeState::err, value);
149             }
150 
151             break;
152 
153         case FRUDataEncoding::bcdPlus:
154             value = std::string();
155             for (i = 0; i < len; i++, iter++)
156             {
157                 uint8_t val = *iter;
158                 value.push_back(bcdPlusToChar(val >> 4));
159                 value.push_back(bcdPlusToChar(val & 0xf));
160             }
161             break;
162 
163         case FRUDataEncoding::sixBitASCII:
164         {
165             unsigned int accum = 0;
166             unsigned int accumBitLen = 0;
167             value = std::string();
168             for (i = 0; i < len; i++, iter++)
169             {
170                 accum |= *iter << accumBitLen;
171                 accumBitLen += 8;
172                 while (accumBitLen >= 6)
173                 {
174                     value.push_back(sixBitToChar(accum & 0x3f));
175                     accum >>= 6;
176                     accumBitLen -= 6;
177                 }
178             }
179         }
180         break;
181 
182         default:
183         {
184             return make_pair(DecodeState::err, value);
185         }
186     }
187 
188     return make_pair(DecodeState::ok, value);
189 }
190 
checkLangEng(uint8_t lang)191 bool checkLangEng(uint8_t lang)
192 {
193     // If Lang is not English then the encoding is defined as 2-byte UNICODE,
194     // but we don't support that.
195     if ((lang != 0U) && lang != 25)
196     {
197         lg2::error("Warning: languages other than English is not supported");
198         // Return language flag as non english
199         return false;
200     }
201     return true;
202 }
203 
204 /* This function verifies for other offsets to check if they are not
205  * falling under other field area
206  *
207  * fruBytes:    Start of Fru data
208  * currentArea: Index of current area offset to be compared against all area
209  *              offset and it is a multiple of 8 bytes as per specification
210  * len:         Length of current area space and it is a multiple of 8 bytes
211  *              as per specification
212  */
verifyOffset(std::span<const uint8_t> fruBytes,fruAreas currentArea,uint8_t len)213 bool verifyOffset(std::span<const uint8_t> fruBytes, fruAreas currentArea,
214                   uint8_t len)
215 {
216     unsigned int fruBytesSize = fruBytes.size();
217 
218     // check if Fru data has at least 8 byte header
219     if (fruBytesSize <= fruBlockSize)
220     {
221         lg2::error("Error: trying to parse empty FRU");
222         return false;
223     }
224 
225     // Check range of passed currentArea value
226     if (currentArea > fruAreas::fruAreaMultirecord)
227     {
228         lg2::error("Error: Fru area is out of range");
229         return false;
230     }
231 
232     unsigned int currentAreaIndex = getHeaderAreaFieldOffset(currentArea);
233     if (currentAreaIndex > fruBytesSize)
234     {
235         lg2::error("Error: Fru area index is out of range");
236         return false;
237     }
238 
239     unsigned int start = fruBytes[currentAreaIndex];
240     unsigned int end = start + len;
241 
242     /* Verify each offset within the range of start and end */
243     for (fruAreas area = fruAreas::fruAreaInternal;
244          area <= fruAreas::fruAreaMultirecord; ++area)
245     {
246         // skip the current offset
247         if (area == currentArea)
248         {
249             continue;
250         }
251 
252         unsigned int areaIndex = getHeaderAreaFieldOffset(area);
253         if (areaIndex > fruBytesSize)
254         {
255             lg2::error("Error: Fru area index is out of range");
256             return false;
257         }
258 
259         unsigned int areaOffset = fruBytes[areaIndex];
260         // if areaOffset is 0 means this area is not available so skip
261         if (areaOffset == 0)
262         {
263             continue;
264         }
265 
266         // check for overlapping of current offset with given areaoffset
267         if (areaOffset == start || (areaOffset > start && areaOffset < end))
268         {
269             lg2::error("{AREA1} offset is overlapping with {AREA2} offset",
270                        "AREA1", getFruAreaName(currentArea), "AREA2",
271                        getFruAreaName(area));
272             return false;
273         }
274     }
275     return true;
276 }
277 
parseMultirecordUUID(std::span<const uint8_t> device,boost::container::flat_map<std::string,std::string> & result)278 static void parseMultirecordUUID(
279     std::span<const uint8_t> device,
280     boost::container::flat_map<std::string, std::string>& result)
281 {
282     constexpr size_t uuidDataLen = 16;
283     constexpr size_t multiRecordHeaderLen = 5;
284     /* UUID record data, plus one to skip past the sub-record type byte */
285     constexpr size_t uuidRecordData = multiRecordHeaderLen + 1;
286     constexpr size_t multiRecordEndOfListMask = 0x80;
287     /* The UUID {00112233-4455-6677-8899-AABBCCDDEEFF} would thus be represented
288      * as: 0x33 0x22 0x11 0x00 0x55 0x44 0x77 0x66 0x88 0x99 0xAA 0xBB 0xCC 0xDD
289      * 0xEE 0xFF
290      */
291     const std::array<uint8_t, uuidDataLen> uuidCharOrder = {
292         3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15};
293     size_t offset = getHeaderAreaFieldOffset(fruAreas::fruAreaMultirecord);
294     if (offset >= device.size())
295     {
296         throw std::runtime_error("Multirecord UUID offset is out of range");
297     }
298     uint32_t areaOffset = device[offset];
299 
300     if (areaOffset == 0)
301     {
302         return;
303     }
304 
305     areaOffset *= fruBlockSize;
306     std::span<const uint8_t>::const_iterator fruBytesIter =
307         device.begin() + areaOffset;
308 
309     /* Verify area offset */
310     if (!verifyOffset(device, fruAreas::fruAreaMultirecord, *fruBytesIter))
311     {
312         return;
313     }
314     while (areaOffset + uuidRecordData + uuidDataLen <= device.size())
315     {
316         if ((areaOffset < device.size()) &&
317             (device[areaOffset] ==
318              (uint8_t)MultiRecordType::managementAccessRecord))
319         {
320             if ((areaOffset + multiRecordHeaderLen < device.size()) &&
321                 (device[areaOffset + multiRecordHeaderLen] ==
322                  (uint8_t)SubManagementAccessRecord::systemUniqueID))
323             {
324                 /* Layout of UUID:
325                  * source: https://www.ietf.org/rfc/rfc4122.txt
326                  *
327                  * UUID binary format (16 bytes):
328                  * 4B-2B-2B-2B-6B (big endian)
329                  *
330                  * UUID string is 36 length of characters (36 bytes):
331                  * 0        9    14   19   24
332                  * xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
333                  *    be     be   be   be       be
334                  * be means it should be converted to big endian.
335                  */
336                 /* Get UUID bytes to UUID string */
337                 std::stringstream tmp;
338                 tmp << std::hex << std::setfill('0');
339                 for (size_t i = 0; i < uuidDataLen; i++)
340                 {
341                     tmp << std::setw(2)
342                         << static_cast<uint16_t>(
343                                device[areaOffset + uuidRecordData +
344                                       uuidCharOrder[i]]);
345                 }
346                 std::string uuidStr = tmp.str();
347                 result["MULTIRECORD_UUID"] =
348                     uuidStr.substr(0, 8) + '-' + uuidStr.substr(8, 4) + '-' +
349                     uuidStr.substr(12, 4) + '-' + uuidStr.substr(16, 4) + '-' +
350                     uuidStr.substr(20, 12);
351                 break;
352             }
353         }
354         if ((device[areaOffset + 1] & multiRecordEndOfListMask) != 0)
355         {
356             break;
357         }
358         areaOffset = areaOffset + device[areaOffset + 2] + multiRecordHeaderLen;
359     }
360 }
361 
decodeField(std::span<const uint8_t>::const_iterator & fruBytesIter,std::span<const uint8_t>::const_iterator & fruBytesIterEndArea,const std::vector<std::string> & fruAreaFieldNames,size_t & fieldIndex,DecodeState & state,bool isLangEng,const fruAreas & area,boost::container::flat_map<std::string,std::string> & result)362 resCodes decodeField(
363     std::span<const uint8_t>::const_iterator& fruBytesIter,
364     std::span<const uint8_t>::const_iterator& fruBytesIterEndArea,
365     const std::vector<std::string>& fruAreaFieldNames, size_t& fieldIndex,
366     DecodeState& state, bool isLangEng, const fruAreas& area,
367     boost::container::flat_map<std::string, std::string>& result)
368 {
369     auto res = decodeFRUData(fruBytesIter, fruBytesIterEndArea, isLangEng);
370     state = res.first;
371     std::string value = res.second;
372     std::string name;
373     bool isCustomField = false;
374     if (fieldIndex < fruAreaFieldNames.size())
375     {
376         name = std::string(getFruAreaName(area)) + "_" +
377                fruAreaFieldNames.at(fieldIndex);
378     }
379     else
380     {
381         isCustomField = true;
382         name = std::string(getFruAreaName(area)) + "_" + fruCustomFieldName +
383                std::to_string(fieldIndex - fruAreaFieldNames.size() + 1);
384     }
385 
386     if (state == DecodeState::ok)
387     {
388         // Strip non null characters and trailing spaces from the end
389         value.erase(
390             std::find_if(value.rbegin(), value.rend(),
391                          [](char ch) { return ((ch != 0) && (ch != ' ')); })
392                 .base(),
393             value.end());
394         if (isCustomField)
395         {
396             // Some MAC addresses are stored in a custom field, with
397             // "MAC:" prefixed on the value.  If we see that, create a
398             // new field with the decoded data
399             if (value.starts_with("MAC: "))
400             {
401                 result["MAC_" + name] = value.substr(5);
402             }
403         }
404         result[name] = std::move(value);
405         ++fieldIndex;
406     }
407     else if (state == DecodeState::err)
408     {
409         lg2::error("Error while parsing {NAME}", "NAME", name);
410 
411         // Cancel decoding if failed to parse any of mandatory
412         // fields
413         if (fieldIndex < fruAreaFieldNames.size())
414         {
415             lg2::error("Failed to parse mandatory field ");
416             return resCodes::resErr;
417         }
418         return resCodes::resWarn;
419     }
420     else
421     {
422         if (fieldIndex < fruAreaFieldNames.size())
423         {
424             lg2::error(
425                 "Mandatory fields absent in FRU area {AREA} after {NAME}",
426                 "AREA", getFruAreaName(area), "NAME", name);
427             return resCodes::resWarn;
428         }
429     }
430     return resCodes::resOK;
431 }
432 
formatIPMIFRU(std::span<const uint8_t> fruBytes,boost::container::flat_map<std::string,std::string> & result)433 resCodes formatIPMIFRU(
434     std::span<const uint8_t> fruBytes,
435     boost::container::flat_map<std::string, std::string>& result)
436 {
437     resCodes ret = resCodes::resOK;
438     if (fruBytes.size() <= fruBlockSize)
439     {
440         lg2::error("Error: trying to parse empty FRU ");
441         return resCodes::resErr;
442     }
443     result["Common_Format_Version"] =
444         std::to_string(static_cast<int>(*fruBytes.begin()));
445 
446     const std::vector<std::string>* fruAreaFieldNames = nullptr;
447 
448     // Don't parse Internal and Multirecord areas
449     for (fruAreas area = fruAreas::fruAreaChassis;
450          area <= fruAreas::fruAreaProduct; ++area)
451     {
452         size_t offset = *(fruBytes.begin() + getHeaderAreaFieldOffset(area));
453         if (offset == 0)
454         {
455             continue;
456         }
457         offset *= fruBlockSize;
458         std::span<const uint8_t>::const_iterator fruBytesIter =
459             fruBytes.begin() + offset;
460         if (fruBytesIter + fruBlockSize >= fruBytes.end())
461         {
462             lg2::error("Not enough data to parse ");
463             return resCodes::resErr;
464         }
465         // check for format version 1
466         if (*fruBytesIter != 0x01)
467         {
468             lg2::error("Unexpected version {VERSION}", "VERSION",
469                        *fruBytesIter);
470             return resCodes::resErr;
471         }
472         ++fruBytesIter;
473 
474         /* Verify other area offset for overlap with current area by passing
475          * length of current area offset pointed by *fruBytesIter
476          */
477         if (!verifyOffset(fruBytes, area, *fruBytesIter))
478         {
479             return resCodes::resErr;
480         }
481 
482         size_t fruAreaSize = *fruBytesIter * fruBlockSize;
483         std::span<const uint8_t>::const_iterator fruBytesIterEndArea =
484             fruBytes.begin() + offset + fruAreaSize - 1;
485         ++fruBytesIter;
486 
487         uint8_t fruComputedChecksum =
488             calculateChecksum(fruBytes.begin() + offset, fruBytesIterEndArea);
489         if (fruComputedChecksum != *fruBytesIterEndArea)
490         {
491             std::stringstream ss;
492             ss << std::hex << std::setfill('0');
493             ss << "Checksum error in FRU area " << getFruAreaName(area) << "\n";
494             ss << "\tComputed checksum: 0x" << std::setw(2)
495                << static_cast<int>(fruComputedChecksum) << "\n";
496             ss << "\tThe read checksum: 0x" << std::setw(2)
497                << static_cast<int>(*fruBytesIterEndArea) << "\n";
498             lg2::error("{ERR}", "ERR", ss.str());
499             ret = resCodes::resWarn;
500         }
501 
502         /* Set default language flag to true as Chassis Fru area are always
503          * encoded in English defined in Section 10 of Fru specification
504          */
505 
506         bool isLangEng = true;
507         switch (area)
508         {
509             case fruAreas::fruAreaChassis:
510             {
511                 result["CHASSIS_TYPE"] =
512                     std::to_string(static_cast<int>(*fruBytesIter));
513                 fruBytesIter += 1;
514                 fruAreaFieldNames = &chassisFruAreas;
515                 break;
516             }
517             case fruAreas::fruAreaBoard:
518             {
519                 uint8_t lang = *fruBytesIter;
520                 result["BOARD_LANGUAGE_CODE"] =
521                     std::to_string(static_cast<int>(lang));
522                 isLangEng = checkLangEng(lang);
523                 fruBytesIter += 1;
524 
525                 unsigned int minutes =
526                     *fruBytesIter | *(fruBytesIter + 1) << 8 |
527                     *(fruBytesIter + 2) << 16;
528                 std::tm fruTime = intelEpoch();
529                 std::time_t timeValue = timegm(&fruTime);
530                 timeValue += static_cast<long>(minutes) * 60;
531                 fruTime = *std::gmtime(&timeValue);
532 
533                 // Tue Nov 20 23:08:00 2018
534                 std::array<char, 32> timeString = {};
535                 auto bytes = std::strftime(timeString.data(), timeString.size(),
536                                            "%Y%m%dT%H%M%SZ", &fruTime);
537                 if (bytes == 0)
538                 {
539                     lg2::error("invalid time string encountered");
540                     return resCodes::resErr;
541                 }
542 
543                 result["BOARD_MANUFACTURE_DATE"] =
544                     std::string_view(timeString.data(), bytes);
545                 fruBytesIter += 3;
546                 fruAreaFieldNames = &boardFruAreas;
547                 break;
548             }
549             case fruAreas::fruAreaProduct:
550             {
551                 uint8_t lang = *fruBytesIter;
552                 result["PRODUCT_LANGUAGE_CODE"] =
553                     std::to_string(static_cast<int>(lang));
554                 isLangEng = checkLangEng(lang);
555                 fruBytesIter += 1;
556                 fruAreaFieldNames = &productFruAreas;
557                 break;
558             }
559             default:
560             {
561                 lg2::error(
562                     "Internal error: unexpected FRU area index: {INDEX} ",
563                     "INDEX", static_cast<int>(area));
564                 return resCodes::resErr;
565             }
566         }
567         size_t fieldIndex = 0;
568         DecodeState state = DecodeState::ok;
569         do
570         {
571             resCodes decodeRet = decodeField(fruBytesIter, fruBytesIterEndArea,
572                                              *fruAreaFieldNames, fieldIndex,
573                                              state, isLangEng, area, result);
574             if (decodeRet == resCodes::resErr)
575             {
576                 return resCodes::resErr;
577             }
578             if (decodeRet == resCodes::resWarn)
579             {
580                 ret = decodeRet;
581             }
582         } while (state == DecodeState::ok);
583         for (; fruBytesIter < fruBytesIterEndArea; fruBytesIter++)
584         {
585             uint8_t c = *fruBytesIter;
586             if (c != 0U)
587             {
588                 lg2::error("Non-zero byte after EndOfFields in FRU area {AREA}",
589                            "AREA", getFruAreaName(area));
590                 ret = resCodes::resWarn;
591                 break;
592             }
593         }
594     }
595 
596     /* Parsing the Multirecord UUID */
597     parseMultirecordUUID(fruBytes, result);
598 
599     return ret;
600 }
601 
602 // Calculate new checksum for fru info area
calculateChecksum(std::span<const uint8_t>::const_iterator iter,std::span<const uint8_t>::const_iterator end)603 uint8_t calculateChecksum(std::span<const uint8_t>::const_iterator iter,
604                           std::span<const uint8_t>::const_iterator end)
605 {
606     constexpr int checksumMod = 256;
607     uint8_t sum = std::accumulate(iter, end, static_cast<uint8_t>(0));
608     return (checksumMod - sum) % checksumMod;
609 }
610 
calculateChecksum(std::span<const uint8_t> fruAreaData)611 uint8_t calculateChecksum(std::span<const uint8_t> fruAreaData)
612 {
613     return calculateChecksum(fruAreaData.begin(), fruAreaData.end());
614 }
615 
616 // Update new fru area length &
617 // Update checksum at new checksum location
618 // Return the offset of the area checksum byte
updateFRUAreaLenAndChecksum(std::vector<uint8_t> & fruData,size_t fruAreaStart,size_t fruAreaEndOfFieldsOffset,size_t fruAreaEndOffset)619 unsigned int updateFRUAreaLenAndChecksum(
620     std::vector<uint8_t>& fruData, size_t fruAreaStart,
621     size_t fruAreaEndOfFieldsOffset, size_t fruAreaEndOffset)
622 {
623     size_t traverseFRUAreaIndex = fruAreaEndOfFieldsOffset - fruAreaStart;
624 
625     // fill zeros for any remaining unused space
626     std::fill(fruData.begin() + fruAreaEndOfFieldsOffset,
627               fruData.begin() + fruAreaEndOffset, 0);
628 
629     size_t mod = traverseFRUAreaIndex % fruBlockSize;
630     size_t checksumLoc = 0;
631     if (mod == 0U)
632     {
633         traverseFRUAreaIndex += (fruBlockSize);
634         checksumLoc = fruAreaEndOfFieldsOffset + (fruBlockSize - 1);
635     }
636     else
637     {
638         traverseFRUAreaIndex += (fruBlockSize - mod);
639         checksumLoc = fruAreaEndOfFieldsOffset + (fruBlockSize - mod - 1);
640     }
641 
642     size_t newFRUAreaLen =
643         (traverseFRUAreaIndex / fruBlockSize) +
644         static_cast<unsigned long>((traverseFRUAreaIndex % fruBlockSize) != 0);
645     size_t fruAreaLengthLoc = fruAreaStart + 1;
646     fruData[fruAreaLengthLoc] = static_cast<uint8_t>(newFRUAreaLen);
647 
648     // Calculate new checksum
649     std::vector<uint8_t> finalFRUData;
650     std::copy_n(fruData.begin() + fruAreaStart, checksumLoc - fruAreaStart,
651                 std::back_inserter(finalFRUData));
652 
653     fruData[checksumLoc] = calculateChecksum(finalFRUData);
654     return checksumLoc;
655 }
656 
getFieldLength(uint8_t fruFieldTypeLenValue)657 ssize_t getFieldLength(uint8_t fruFieldTypeLenValue)
658 {
659     constexpr uint8_t typeLenMask = 0x3F;
660     constexpr uint8_t endOfFields = 0xC1;
661     if (fruFieldTypeLenValue == endOfFields)
662     {
663         return -1;
664     }
665     return fruFieldTypeLenValue & typeLenMask;
666 }
667 
validateHeader(const std::array<uint8_t,I2C_SMBUS_BLOCK_MAX> & blockData)668 bool validateHeader(const std::array<uint8_t, I2C_SMBUS_BLOCK_MAX>& blockData)
669 {
670     // ipmi spec format version number is currently at 1, verify it
671     if (blockData[0] != fruVersion)
672     {
673         lg2::debug(
674             "FRU spec version {VERSION} not supported. Supported version is {SUPPORTED_VERSION}",
675             "VERSION", lg2::hex, blockData[0], "SUPPORTED_VERSION", lg2::hex,
676             fruVersion);
677         return false;
678     }
679 
680     // verify pad is set to 0
681     if (blockData[6] != 0x0)
682     {
683         lg2::debug("Pad value in header is non zero, value is {VALUE}", "VALUE",
684                    lg2::hex, blockData[6]);
685         return false;
686     }
687 
688     // verify offsets are 0, or don't point to another offset
689     std::set<uint8_t> foundOffsets;
690     for (int ii = 1; ii < 6; ii++)
691     {
692         if (blockData[ii] == 0)
693         {
694             continue;
695         }
696         auto inserted = foundOffsets.insert(blockData[ii]);
697         if (!inserted.second)
698         {
699             return false;
700         }
701     }
702 
703     // validate checksum
704     size_t sum = 0;
705     for (int jj = 0; jj < 7; jj++)
706     {
707         sum += blockData[jj];
708     }
709     sum = (256 - sum) & 0xFF;
710 
711     if (sum != blockData[7])
712     {
713         lg2::debug(
714             "Checksum {CHECKSUM} is invalid. calculated checksum is {CALCULATED_CHECKSUM}",
715             "CHECKSUM", lg2::hex, blockData[7], "CALCULATED_CHECKSUM", lg2::hex,
716             sum);
717         return false;
718     }
719     return true;
720 }
721 
parseMacFromGzipXmlHeader(FRUReader & reader,off_t offset)722 std::string parseMacFromGzipXmlHeader(FRUReader& reader, off_t offset)
723 {
724     // gzip starts at offset 512. Read that from the FRU
725     // in this case, 32k bytes is enough to hold the whole manifest
726     constexpr size_t totalReadSize = 32UL * 1024UL;
727 
728     std::vector<uint8_t> headerData(totalReadSize, 0U);
729 
730     int rc = reader.read(offset, totalReadSize, headerData.data());
731     if (rc <= 0)
732     {
733         return {};
734     }
735 
736     std::optional<std::string> xml = gzipInflate(headerData);
737     if (!xml)
738     {
739         return {};
740     }
741     std::vector<std::string> node = getNodeFromXml(
742         *xml, "/GSSKU/BoardInfo/Main/NIC/*[Mode = 'Dedicated']/MacAddr0");
743     if (node.empty())
744     {
745         lg2::debug("No mac address found in gzip xml header");
746         return {};
747     }
748     if (node.size() > 1)
749     {
750         lg2::warning("Multiple mac addresses found in gzip xml header");
751     }
752     return node[0];
753 }
754 
findFRUHeader(FRUReader & reader,const std::string & errorHelp,off_t startingOffset)755 std::optional<FruSections> findFRUHeader(
756     FRUReader& reader, const std::string& errorHelp, off_t startingOffset)
757 {
758     std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData = {};
759     if (reader.read(startingOffset, 0x8, blockData.data()) < 0)
760     {
761         lg2::error("failed to read {ERR} base offset {OFFSET}", "ERR",
762                    errorHelp, "OFFSET", startingOffset);
763         return std::nullopt;
764     }
765 
766     // check the header checksum
767     if (validateHeader(blockData))
768     {
769         FruSections fru = {};
770         static_assert(fru.ipmiFruBlock.size() == blockData.size(),
771                       "size mismatch in block data");
772         std::memcpy(fru.ipmiFruBlock.data(), blockData.data(),
773                     I2C_SMBUS_BLOCK_MAX);
774         fru.IpmiFruOffset = startingOffset;
775         return fru;
776     }
777 
778     // only continue the search if we just looked at 0x0.
779     if (startingOffset != 0)
780     {
781         return std::nullopt;
782     }
783 
784     // now check for special cases where the IPMI data is at an offset
785 
786     // check if blockData starts with tyanHeader
787     const std::vector<uint8_t> tyanHeader = {'$', 'T', 'Y', 'A', 'N', '$'};
788     if (blockData.size() >= tyanHeader.size() &&
789         std::equal(tyanHeader.begin(), tyanHeader.end(), blockData.begin()))
790     {
791         // look for the FRU header at offset 0x6000
792         off_t tyanOffset = 0x6000;
793         return findFRUHeader(reader, errorHelp, tyanOffset);
794     }
795 
796     // check if blockData starts with gigabyteHeader
797     const std::vector<uint8_t> gigabyteHeader = {'G', 'I', 'G', 'A',
798                                                  'B', 'Y', 'T', 'E'};
799     if (blockData.size() >= gigabyteHeader.size() &&
800         std::equal(gigabyteHeader.begin(), gigabyteHeader.end(),
801                    blockData.begin()))
802     {
803         // look for the FRU header at offset 0x4000
804         off_t gbOffset = 0x4000;
805         auto sections = findFRUHeader(reader, errorHelp, gbOffset);
806         if (sections)
807         {
808             lg2::debug("succeeded on GB parse");
809             // GB xml header is at 512 bytes
810             sections->GigabyteXmlOffset = 512;
811         }
812         else
813         {
814             lg2::error("Failed on GB parse");
815         }
816         return sections;
817     }
818 
819     lg2::debug("Illegal header {HEADER} base offset {OFFSET}", "HEADER",
820                errorHelp, "OFFSET", startingOffset);
821 
822     return std::nullopt;
823 }
824 
readFRUContents(FRUReader & reader,const std::string & errorHelp)825 std::pair<std::vector<uint8_t>, bool> readFRUContents(
826     FRUReader& reader, const std::string& errorHelp)
827 {
828     std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
829     std::optional<FruSections> sections = findFRUHeader(reader, errorHelp, 0);
830     if (!sections)
831     {
832         return {{}, false};
833     }
834     const off_t baseOffset = sections->IpmiFruOffset;
835     std::memcpy(blockData.data(), sections->ipmiFruBlock.data(),
836                 blockData.size());
837     std::vector<uint8_t> device;
838     device.insert(device.end(), blockData.begin(),
839                   std::next(blockData.begin(), 8));
840 
841     bool hasMultiRecords = false;
842     size_t fruLength = fruBlockSize; // At least FRU header is present
843     unsigned int prevOffset = 0;
844     for (fruAreas area = fruAreas::fruAreaInternal;
845          area <= fruAreas::fruAreaMultirecord; ++area)
846     {
847         // Offset value can be 255.
848         unsigned int areaOffset = device[getHeaderAreaFieldOffset(area)];
849         if (areaOffset == 0)
850         {
851             continue;
852         }
853 
854         /* Check for offset order, as per Section 17 of FRU specification, FRU
855          * information areas are required to be in order in FRU data layout
856          * which means all offset value should be in increasing order or can be
857          * 0 if that area is not present
858          */
859         if (areaOffset <= prevOffset)
860         {
861             lg2::error(
862                 "Fru area offsets are not in required order as per Section 17 of Fru specification");
863             return {{}, true};
864         }
865         prevOffset = areaOffset;
866 
867         // MultiRecords are different. area is not tracking section, it's
868         // walking the common header.
869         if (area == fruAreas::fruAreaMultirecord)
870         {
871             hasMultiRecords = true;
872             break;
873         }
874 
875         areaOffset *= fruBlockSize;
876 
877         if (reader.read(baseOffset + areaOffset, 0x2, blockData.data()) < 0)
878         {
879             lg2::error("failed to read {ERR} base offset {OFFSET}", "ERR",
880                        errorHelp, "OFFSET", baseOffset);
881             return {{}, true};
882         }
883 
884         // Ignore data type (blockData is already unsigned).
885         size_t length = blockData[1] * fruBlockSize;
886         areaOffset += length;
887         fruLength = (areaOffset > fruLength) ? areaOffset : fruLength;
888     }
889 
890     if (hasMultiRecords)
891     {
892         // device[area count] is the index to the last area because the 0th
893         // entry is not an offset in the common header.
894         unsigned int areaOffset =
895             device[getHeaderAreaFieldOffset(fruAreas::fruAreaMultirecord)];
896         areaOffset *= fruBlockSize;
897 
898         // the multi-area record header is 5 bytes long.
899         constexpr size_t multiRecordHeaderSize = 5;
900         constexpr uint8_t multiRecordEndOfListMask = 0x80;
901 
902         // Sanity hard-limit to 64KB.
903         while (areaOffset < std::numeric_limits<uint16_t>::max())
904         {
905             // In multi-area, the area offset points to the 0th record, each
906             // record has 3 bytes of the header we care about.
907             if (reader.read(baseOffset + areaOffset, 0x3, blockData.data()) < 0)
908             {
909                 lg2::error("failed to read {STR} base offset {OFFSET}", "STR",
910                            errorHelp, "OFFSET", baseOffset);
911                 return {{}, true};
912             }
913 
914             // Ok, let's check the record length, which is in bytes (unsigned,
915             // up to 255, so blockData should hold uint8_t not char)
916             size_t recordLength = blockData[2];
917             areaOffset += (recordLength + multiRecordHeaderSize);
918             fruLength = (areaOffset > fruLength) ? areaOffset : fruLength;
919 
920             // If this is the end of the list bail.
921             if ((blockData[1] & multiRecordEndOfListMask) != 0)
922             {
923                 break;
924             }
925         }
926     }
927 
928     // You already copied these first 8 bytes (the ipmi fru header size)
929     fruLength -= std::min(fruBlockSize, fruLength);
930 
931     int readOffset = fruBlockSize;
932 
933     while (fruLength > 0)
934     {
935         size_t requestLength =
936             std::min(static_cast<size_t>(I2C_SMBUS_BLOCK_MAX), fruLength);
937 
938         if (reader.read(baseOffset + readOffset, requestLength,
939                         blockData.data()) < 0)
940         {
941             lg2::error("failed to read {ERR} base offset {OFFSET}", "ERR",
942                        errorHelp, "OFFSET", baseOffset);
943             return {{}, true};
944         }
945 
946         device.insert(device.end(), blockData.begin(),
947                       std::next(blockData.begin(), requestLength));
948 
949         readOffset += requestLength;
950         fruLength -= std::min(requestLength, fruLength);
951     }
952 
953     if (sections->GigabyteXmlOffset != 0)
954     {
955         std::string macAddress =
956             parseMacFromGzipXmlHeader(reader, sections->GigabyteXmlOffset);
957         if (!macAddress.empty())
958         {
959             // launder the mac address as we expect into
960             // BOARD_INFO_AM2 to allow the rest of the
961             // system to use it
962             std::string mac = std::format("MAC: {}", macAddress);
963             updateAddProperty(mac, "BOARD_INFO_AM2", device);
964         }
965     }
966 
967     return {device, true};
968 }
969 
getHeaderAreaFieldOffset(fruAreas area)970 unsigned int getHeaderAreaFieldOffset(fruAreas area)
971 {
972     return static_cast<unsigned int>(area) + 1;
973 }
974 
getFRUInfo(const uint16_t & bus,const uint8_t & address)975 std::vector<uint8_t>& getFRUInfo(const uint16_t& bus, const uint8_t& address)
976 {
977     auto deviceMap = busMap.find(bus);
978     if (deviceMap == busMap.end())
979     {
980         throw std::invalid_argument("Invalid Bus.");
981     }
982     auto device = deviceMap->second->find(address);
983     if (device == deviceMap->second->end())
984     {
985         throw std::invalid_argument("Invalid Address.");
986     }
987     std::vector<uint8_t>& ret = device->second;
988 
989     return ret;
990 }
991 
updateHeaderChecksum(std::vector<uint8_t> & fruData)992 static bool updateHeaderChecksum(std::vector<uint8_t>& fruData)
993 {
994     if (fruData.size() < fruBlockSize)
995     {
996         lg2::debug("FRU data is too small to contain a valid header.");
997         return false;
998     }
999 
1000     uint8_t& checksumInBytes = fruData[7];
1001     uint8_t checksum =
1002         calculateChecksum({fruData.begin(), fruData.begin() + 7});
1003     std::swap(checksumInBytes, checksum);
1004 
1005     if (checksumInBytes != checksum)
1006     {
1007         lg2::debug(
1008             "FRU header checksum updated from {OLD_CHECKSUM} to {NEW_CHECKSUM}",
1009             "OLD_CHECKSUM", static_cast<int>(checksum), "NEW_CHECKSUM",
1010             static_cast<int>(checksumInBytes));
1011     }
1012     return true;
1013 }
1014 
updateAreaChecksum(std::vector<uint8_t> & fruArea)1015 bool updateAreaChecksum(std::vector<uint8_t>& fruArea)
1016 {
1017     if (fruArea.size() < fruBlockSize)
1018     {
1019         lg2::debug("FRU area is too small to contain a valid header.");
1020         return false;
1021     }
1022     if (fruArea.size() % fruBlockSize != 0)
1023     {
1024         lg2::debug("FRU area size is not a multiple of {SIZE} bytes.", "SIZE",
1025                    fruBlockSize);
1026         return false;
1027     }
1028 
1029     uint8_t oldcksum = fruArea[fruArea.size() - 1];
1030 
1031     fruArea[fruArea.size() - 1] =
1032         0; // Reset checksum byte to 0 before recalculating
1033     fruArea[fruArea.size() - 1] = calculateChecksum(fruArea);
1034 
1035     if (oldcksum != fruArea[fruArea.size() - 1])
1036     {
1037         lg2::debug(
1038             "FRU area checksum updated from {OLD_CHECKSUM} to {NEW_CHECKSUM}",
1039             "OLD_CHECKSUM", static_cast<int>(oldcksum), "NEW_CHECKSUM",
1040             static_cast<int>(fruArea[fruArea.size() - 1]));
1041     }
1042     return true;
1043 }
1044 
calculateAreaSize(fruAreas area,std::span<const uint8_t> fruData,size_t areaOffset)1045 static std::optional<size_t> calculateAreaSize(
1046     fruAreas area, std::span<const uint8_t> fruData, size_t areaOffset)
1047 {
1048     switch (area)
1049     {
1050         case fruAreas::fruAreaChassis:
1051         case fruAreas::fruAreaBoard:
1052         case fruAreas::fruAreaProduct:
1053             if (areaOffset + 1 >= fruData.size())
1054             {
1055                 return std::nullopt;
1056             }
1057             return fruData[areaOffset + 1] * fruBlockSize; // Area size in bytes
1058         case fruAreas::fruAreaInternal:
1059         {
1060             // Internal area size: It is difference between the next area
1061             // offset and current area offset
1062             for (fruAreas areaIt = fruAreas::fruAreaChassis;
1063                  areaIt <= fruAreas::fruAreaMultirecord; ++areaIt)
1064             {
1065                 size_t headerOffset = getHeaderAreaFieldOffset(areaIt);
1066                 if (headerOffset >= fruData.size())
1067                 {
1068                     return std::nullopt;
1069                 }
1070                 size_t nextAreaOffset = fruData[headerOffset];
1071                 if (nextAreaOffset != 0)
1072                 {
1073                     return nextAreaOffset * fruBlockSize - areaOffset;
1074                 }
1075             }
1076             return std::nullopt;
1077         }
1078         break;
1079         case fruAreas::fruAreaMultirecord:
1080             // Multirecord area size.
1081             return fruData.size() - areaOffset; // Area size in bytes
1082         default:
1083             lg2::error("Invalid FRU area: {AREA}", "AREA",
1084                        static_cast<int>(area));
1085     }
1086     return std::nullopt;
1087 }
1088 
getBlockCount(size_t byteCount)1089 static size_t getBlockCount(size_t byteCount)
1090 {
1091     size_t blocks = (byteCount + fruBlockSize - 1) / fruBlockSize;
1092     // if we're perfectly aligned, we need another block for the checksum
1093     if ((byteCount % fruBlockSize) == 0)
1094     {
1095         blocks++;
1096     }
1097     return blocks;
1098 }
1099 
disassembleFruData(std::vector<uint8_t> & fruData,std::vector<std::vector<uint8_t>> & areasData)1100 bool disassembleFruData(std::vector<uint8_t>& fruData,
1101                         std::vector<std::vector<uint8_t>>& areasData)
1102 {
1103     if (fruData.size() < 8)
1104     {
1105         lg2::debug("FRU data is too small to contain a valid header.");
1106         return false;
1107     }
1108 
1109     // Clear areasData before disassembling
1110     areasData.clear();
1111 
1112     // Iterate through all areas & store each area data in a vector.
1113     for (fruAreas area = fruAreas::fruAreaInternal;
1114          area <= fruAreas::fruAreaMultirecord; ++area)
1115     {
1116         size_t areaOffset = fruData[getHeaderAreaFieldOffset(area)];
1117 
1118         if (areaOffset == 0)
1119         {
1120             // Store empty area data for areas that are not present
1121             areasData.emplace_back();
1122             continue;               // Skip areas that are not present
1123         }
1124         areaOffset *= fruBlockSize; // Convert to byte offset
1125 
1126         std::optional<size_t> areaSize =
1127             calculateAreaSize(area, fruData, areaOffset);
1128         if (!areaSize)
1129         {
1130             return false;
1131         }
1132 
1133         if ((areaOffset + *areaSize) > fruData.size())
1134         {
1135             lg2::error("Area offset + size exceeds FRU data size.");
1136             return false;
1137         }
1138 
1139         areasData.emplace_back(fruData.begin() + areaOffset,
1140                                fruData.begin() + areaOffset + *areaSize);
1141     }
1142 
1143     return true;
1144 }
1145 
1146 struct FieldInfo
1147 {
1148     size_t length;
1149     size_t index;
1150 };
1151 
findOrCreateField(std::vector<uint8_t> & areaData,const std::string & propertyName,const fruAreas & fruAreaToUpdate)1152 static std::optional<FieldInfo> findOrCreateField(
1153     std::vector<uint8_t>& areaData, const std::string& propertyName,
1154     const fruAreas& fruAreaToUpdate)
1155 {
1156     int fieldIndex = 0;
1157     int fieldLength = 0;
1158     std::string areaName = propertyName.substr(0, propertyName.find('_'));
1159     std::string propertyNamePrefix = areaName + "_";
1160     const std::vector<std::string>* fruAreaFieldNames = nullptr;
1161 
1162     switch (fruAreaToUpdate)
1163     {
1164         case fruAreas::fruAreaChassis:
1165             fruAreaFieldNames = &chassisFruAreas;
1166             fieldIndex = 3;
1167             break;
1168         case fruAreas::fruAreaBoard:
1169             fruAreaFieldNames = &boardFruAreas;
1170             fieldIndex = 6;
1171             break;
1172         case fruAreas::fruAreaProduct:
1173             fruAreaFieldNames = &productFruAreas;
1174             fieldIndex = 3;
1175             break;
1176         default:
1177             lg2::info("Invalid FRU area: {AREA}", "AREA",
1178                       static_cast<int>(fruAreaToUpdate));
1179             return std::nullopt;
1180     }
1181 
1182     for (const auto& field : *fruAreaFieldNames)
1183     {
1184         fieldLength = getFieldLength(areaData[fieldIndex]);
1185         if (fieldLength < 0)
1186         {
1187             areaData.insert(areaData.begin() + fieldIndex, 0xc0);
1188             fieldLength = 0;
1189         }
1190 
1191         if (propertyNamePrefix + field == propertyName)
1192         {
1193             return FieldInfo{static_cast<size_t>(fieldLength),
1194                              static_cast<size_t>(fieldIndex)};
1195         }
1196         fieldIndex += 1 + fieldLength;
1197     }
1198 
1199     size_t pos = propertyName.find(fruCustomFieldName);
1200     if (pos == std::string::npos)
1201     {
1202         return std::nullopt;
1203     }
1204 
1205     // Get field after pos
1206     std::string customFieldIdx =
1207         propertyName.substr(pos + fruCustomFieldName.size());
1208 
1209     // Check if customFieldIdx is a number
1210     if (!std::all_of(customFieldIdx.begin(), customFieldIdx.end(), ::isdigit))
1211     {
1212         return std::nullopt;
1213     }
1214 
1215     size_t customFieldIndex = std::stoi(customFieldIdx);
1216 
1217     // insert custom fields up to the index we want
1218     for (size_t i = 0; i < customFieldIndex; i++)
1219     {
1220         fieldLength = getFieldLength(areaData[fieldIndex]);
1221         if (fieldLength < 0)
1222         {
1223             areaData.insert(areaData.begin() + fieldIndex, 0xc0);
1224             fieldLength = 0;
1225         }
1226         fieldIndex += 1 + fieldLength;
1227     }
1228 
1229     fieldIndex -= (fieldLength + 1);
1230     fieldLength = getFieldLength(areaData[fieldIndex]);
1231     return FieldInfo{static_cast<size_t>(fieldLength),
1232                      static_cast<size_t>(fieldIndex)};
1233 }
1234 
findEndOfFieldMarker(std::span<uint8_t> bytes)1235 static std::optional<size_t> findEndOfFieldMarker(std::span<uint8_t> bytes)
1236 {
1237     // we're skipping the checksum
1238     // this function assumes a properly sized and formatted area
1239     static uint8_t constexpr endOfFieldsByte = 0xc1;
1240     for (int index = bytes.size() - 2; index >= 0; --index)
1241     {
1242         if (bytes[index] == endOfFieldsByte)
1243         {
1244             return index;
1245         }
1246     }
1247     return std::nullopt;
1248 }
1249 
getNonPaddedSizeOfArea(std::span<uint8_t> bytes)1250 static std::optional<size_t> getNonPaddedSizeOfArea(std::span<uint8_t> bytes)
1251 {
1252     if (auto endOfFields = findEndOfFieldMarker(bytes))
1253     {
1254         return *endOfFields + 1;
1255     }
1256     return std::nullopt;
1257 }
1258 
setField(const fruAreas & fruAreaToUpdate,std::vector<uint8_t> & areaData,const std::string & propertyName,const std::string & value)1259 bool setField(const fruAreas& fruAreaToUpdate, std::vector<uint8_t>& areaData,
1260               const std::string& propertyName, const std::string& value)
1261 {
1262     if (value.size() == 1 || value.size() > 63)
1263     {
1264         lg2::error("Invalid value {VALUE} for field {PROP}", "VALUE", value,
1265                    "PROP", propertyName);
1266         return false;
1267     }
1268 
1269     // This is inneficient, but the alternative requires
1270     // a bunch of complicated indexing and search to
1271     // figure out if we cross a block boundary
1272     // if we feel that this is too inneficient in the future,
1273     // we can implement that.
1274     std::vector<uint8_t> tmpBuffer = areaData;
1275 
1276     auto fieldInfo =
1277         findOrCreateField(tmpBuffer, propertyName, fruAreaToUpdate);
1278 
1279     if (!fieldInfo)
1280     {
1281         lg2::error("Field {FIELD} not found in area {AREA}", "FIELD",
1282                    propertyName, "AREA", getFruAreaName(fruAreaToUpdate));
1283         return false;
1284     }
1285 
1286     auto fieldIt = tmpBuffer.begin() + fieldInfo->index;
1287     // Erase the existing field content.
1288     tmpBuffer.erase(fieldIt, fieldIt + fieldInfo->length + 1);
1289     // Insert the new field value
1290     tmpBuffer.insert(fieldIt, 0xc0 | value.size());
1291     tmpBuffer.insert_range(fieldIt + 1, value);
1292 
1293     auto newSize = getNonPaddedSizeOfArea(tmpBuffer);
1294     auto oldSize = getNonPaddedSizeOfArea(areaData);
1295 
1296     if (!oldSize || !newSize)
1297     {
1298         lg2::error("Failed to find the size of the area");
1299         return false;
1300     }
1301 
1302     size_t newSizePadded = getBlockCount(*newSize);
1303 #ifndef ENABLE_FRU_AREA_RESIZE
1304 
1305     size_t oldSizePadded = getBlockCount(*oldSize);
1306 
1307     if (newSizePadded != oldSizePadded)
1308     {
1309         lg2::error(
1310             "FRU area {AREA} resize is disabled, cannot increase size from {OLD_SIZE} to {NEW_SIZE}",
1311             "AREA", getFruAreaName(fruAreaToUpdate), "OLD_SIZE",
1312             static_cast<int>(oldSizePadded), "NEW_SIZE",
1313             static_cast<int>(newSizePadded));
1314         return false;
1315     }
1316 #endif
1317     // Resize the buffer as per numOfBlocks & pad with zeros
1318     tmpBuffer.resize(newSizePadded * fruBlockSize, 0);
1319 
1320     // Update the length field
1321     tmpBuffer[1] = newSizePadded;
1322     updateAreaChecksum(tmpBuffer);
1323 
1324     areaData = std::move(tmpBuffer);
1325 
1326     return true;
1327 }
1328 
assembleFruData(std::vector<uint8_t> & fruData,const std::vector<std::vector<uint8_t>> & areasData)1329 bool assembleFruData(std::vector<uint8_t>& fruData,
1330                      const std::vector<std::vector<uint8_t>>& areasData)
1331 {
1332     for (const auto& area : areasData)
1333     {
1334         if ((area.size() % fruBlockSize) != 0U)
1335         {
1336             lg2::error("unaligned area sent to assembleFruData");
1337             return false;
1338         }
1339     }
1340 
1341     // Clear the existing FRU data
1342     fruData.clear();
1343     fruData.resize(8); // Start with the header size
1344 
1345     // Write the header
1346     fruData[0] = fruVersion; // Version
1347     fruData[1] = 0;          // Internal area offset
1348     fruData[2] = 0;          // Chassis area offset
1349     fruData[3] = 0;          // Board area offset
1350     fruData[4] = 0;          // Product area offset
1351     fruData[5] = 0;          // Multirecord area offset
1352     fruData[6] = 0;          // Pad
1353     fruData[7] = 0;          // Checksum (to be updated later)
1354 
1355     size_t writeOffset = 8;  // Start writing after the header
1356 
1357     for (fruAreas area = fruAreas::fruAreaInternal;
1358          area <= fruAreas::fruAreaMultirecord; ++area)
1359     {
1360         const auto& areaBytes = areasData[static_cast<size_t>(area)];
1361 
1362         if (areaBytes.empty())
1363         {
1364             lg2::debug("Skipping empty area: {AREA}", "AREA",
1365                        getFruAreaName(area));
1366             continue; // Skip areas that are not present
1367         }
1368 
1369         // Set the area offset in the header
1370         fruData[getHeaderAreaFieldOffset(area)] = writeOffset / fruBlockSize;
1371         fruData.append_range(areaBytes);
1372         writeOffset += areaBytes.size();
1373     }
1374 
1375     // Update the header checksum
1376     if (!updateHeaderChecksum(fruData))
1377     {
1378         lg2::error("failed to update header checksum");
1379         return false;
1380     }
1381 
1382     return true;
1383 }
1384 
1385 // Create a dummy area in areData variable based on specified fruArea
createDummyArea(fruAreas fruArea,std::vector<uint8_t> & areaData)1386 bool createDummyArea(fruAreas fruArea, std::vector<uint8_t>& areaData)
1387 {
1388     uint8_t numOfFields = 0;
1389     uint8_t numOfBlocks = 0;
1390     // Clear the areaData vector
1391     areaData.clear();
1392 
1393     // Set the version, length, and other fields
1394     areaData.push_back(fruVersion); // Version 1
1395     areaData.push_back(0);          // Length (to be updated later)
1396 
1397     switch (fruArea)
1398     {
1399         case fruAreas::fruAreaChassis:
1400             areaData.push_back(0x00); // Chassis type
1401             numOfFields = chassisFruAreas.size();
1402             break;
1403         case fruAreas::fruAreaBoard:
1404             areaData.push_back(0x00); // Board language code (default)
1405             areaData.insert(areaData.end(),
1406                             {0x00, 0x00,
1407                              0x00}); // Board manufacturer date (default)
1408             numOfFields = boardFruAreas.size();
1409             break;
1410         case fruAreas::fruAreaProduct:
1411             areaData.push_back(0x00); // Product language code (default)
1412             numOfFields = productFruAreas.size();
1413             break;
1414         default:
1415             lg2::debug("Invalid FRU area to create: {AREA}", "AREA",
1416                        static_cast<int>(fruArea));
1417             return false;
1418     }
1419 
1420     for (size_t i = 0; i < numOfFields; ++i)
1421     {
1422         areaData.push_back(0xc0); // Empty field type
1423     }
1424 
1425     // Add EndOfFields marker
1426     areaData.push_back(0xC1);
1427     numOfBlocks = (areaData.size() + fruBlockSize - 1) /
1428                   fruBlockSize; // Calculate number of blocks needed
1429     areaData.resize(numOfBlocks * fruBlockSize, 0); // Fill with zeros
1430     areaData[1] = numOfBlocks;                      // Update length field
1431     updateAreaChecksum(areaData);
1432 
1433     return true;
1434 }
1435 
1436 // Iterate FruArea Names and find start and size of the fru area that contains
1437 // the propertyName and the field start location for the property. fruAreaParams
1438 // struct values fruAreaStart, fruAreaSize, fruAreaEnd, fieldLoc values gets
1439 // updated/returned if successful.
1440 
findFruAreaLocationAndField(std::vector<uint8_t> & fruData,const std::string & propertyName,struct FruArea & fruAreaParams)1441 bool findFruAreaLocationAndField(std::vector<uint8_t>& fruData,
1442                                  const std::string& propertyName,
1443                                  struct FruArea& fruAreaParams)
1444 {
1445     const std::vector<std::string>* fruAreaFieldNames = nullptr;
1446 
1447     uint8_t fruAreaOffsetFieldValue = 0;
1448     size_t offset = 0;
1449     std::string areaName = propertyName.substr(0, propertyName.find('_'));
1450     std::string propertyNamePrefix = areaName + "_";
1451     auto it = std::find(fruAreaNames.begin(), fruAreaNames.end(), areaName);
1452     if (it == fruAreaNames.end())
1453     {
1454         lg2::error("Can't parse area name for property {PROP} ", "PROP",
1455                    propertyName);
1456         return false;
1457     }
1458     fruAreas fruAreaToUpdate = static_cast<fruAreas>(it - fruAreaNames.begin());
1459     fruAreaOffsetFieldValue =
1460         fruData[getHeaderAreaFieldOffset(fruAreaToUpdate)];
1461     switch (fruAreaToUpdate)
1462     {
1463         case fruAreas::fruAreaChassis:
1464             offset = 3; // chassis part number offset. Skip fixed first 3 bytes
1465             fruAreaFieldNames = &chassisFruAreas;
1466             break;
1467         case fruAreas::fruAreaBoard:
1468             offset = 6; // board manufacturer offset. Skip fixed first 6 bytes
1469             fruAreaFieldNames = &boardFruAreas;
1470             break;
1471         case fruAreas::fruAreaProduct:
1472             // Manufacturer name offset. Skip fixed first 3 product fru bytes
1473             // i.e. version, area length and language code
1474             offset = 3;
1475             fruAreaFieldNames = &productFruAreas;
1476             break;
1477         default:
1478             lg2::error("Invalid PropertyName {PROP}", "PROP", propertyName);
1479             return false;
1480     }
1481     if (fruAreaOffsetFieldValue == 0)
1482     {
1483         lg2::error("FRU Area for {PROP} not present ", "PROP", propertyName);
1484         return false;
1485     }
1486 
1487     fruAreaParams.start = fruAreaOffsetFieldValue * fruBlockSize;
1488     fruAreaParams.size = fruData[fruAreaParams.start + 1] * fruBlockSize;
1489     fruAreaParams.end = fruAreaParams.start + fruAreaParams.size;
1490     size_t fruDataIter = fruAreaParams.start + offset;
1491     size_t skipToFRUUpdateField = 0;
1492     ssize_t fieldLength = 0;
1493 
1494     bool found = false;
1495     for (const auto& field : *fruAreaFieldNames)
1496     {
1497         skipToFRUUpdateField++;
1498         if (propertyName == propertyNamePrefix + field)
1499         {
1500             found = true;
1501             break;
1502         }
1503     }
1504     if (!found)
1505     {
1506         std::size_t pos = propertyName.find(fruCustomFieldName);
1507         if (pos == std::string::npos)
1508         {
1509             lg2::error("PropertyName doesn't exist in FRU Area Vectors: {PROP}",
1510                        "PROP", propertyName);
1511             return false;
1512         }
1513         std::string fieldNumStr =
1514             propertyName.substr(pos + fruCustomFieldName.length());
1515         size_t fieldNum = std::stoi(fieldNumStr);
1516         if (fieldNum == 0)
1517         {
1518             lg2::error("PropertyName not recognized: {PROP}", "PROP",
1519                        propertyName);
1520             return false;
1521         }
1522         skipToFRUUpdateField += fieldNum;
1523     }
1524 
1525     for (size_t i = 1; i < skipToFRUUpdateField; i++)
1526     {
1527         if (fruDataIter < fruData.size())
1528         {
1529             fieldLength = getFieldLength(fruData[fruDataIter]);
1530 
1531             if (fieldLength < 0)
1532             {
1533                 break;
1534             }
1535             fruDataIter += 1 + fieldLength;
1536         }
1537     }
1538     fruAreaParams.updateFieldLoc = fruDataIter;
1539 
1540     return true;
1541 }
1542 
1543 // Copy the FRU Area fields and properties into restFRUAreaFieldsData vector.
1544 // Return true for success and false for failure.
1545 
copyRestFRUArea(std::vector<uint8_t> & fruData,const std::string & propertyName,struct FruArea & fruAreaParams,std::vector<uint8_t> & restFRUAreaFieldsData)1546 bool copyRestFRUArea(std::vector<uint8_t>& fruData,
1547                      const std::string& propertyName,
1548                      struct FruArea& fruAreaParams,
1549                      std::vector<uint8_t>& restFRUAreaFieldsData)
1550 {
1551     size_t fieldLoc = fruAreaParams.updateFieldLoc;
1552     size_t start = fruAreaParams.start;
1553     size_t fruAreaSize = fruAreaParams.size;
1554 
1555     // Push post update fru field bytes to a vector
1556     ssize_t fieldLength = getFieldLength(fruData[fieldLoc]);
1557     if (fieldLength < 0)
1558     {
1559         lg2::error("Property {PROP} not present ", "PROP", propertyName);
1560         return false;
1561     }
1562 
1563     size_t fruDataIter = 0;
1564     fruDataIter = fieldLoc;
1565     fruDataIter += 1 + fieldLength;
1566     size_t restFRUFieldsLoc = fruDataIter;
1567     size_t endOfFieldsLoc = 0;
1568 
1569     if (fruDataIter < fruData.size())
1570     {
1571         while ((fieldLength = getFieldLength(fruData[fruDataIter])) >= 0)
1572         {
1573             if (fruDataIter >= (start + fruAreaSize))
1574             {
1575                 fruDataIter = start + fruAreaSize;
1576                 break;
1577             }
1578             fruDataIter += 1 + fieldLength;
1579         }
1580         endOfFieldsLoc = fruDataIter;
1581     }
1582 
1583     std::copy_n(fruData.begin() + restFRUFieldsLoc,
1584                 endOfFieldsLoc - restFRUFieldsLoc + 1,
1585                 std::back_inserter(restFRUAreaFieldsData));
1586 
1587     fruAreaParams.restFieldsLoc = restFRUFieldsLoc;
1588     fruAreaParams.restFieldsEnd = endOfFieldsLoc;
1589 
1590     return true;
1591 }
1592 
1593 // Get all device dbus path and match path with product name using
1594 // regular expression and find the device index for all devices.
1595 
findIndexForFRU(boost::container::flat_map<std::pair<size_t,size_t>,std::shared_ptr<sdbusplus::asio::dbus_interface>> & dbusInterfaceMap,std::string & productName)1596 std::optional<int> findIndexForFRU(
1597     boost::container::flat_map<
1598         std::pair<size_t, size_t>,
1599         std::shared_ptr<sdbusplus::asio::dbus_interface>>& dbusInterfaceMap,
1600     std::string& productName)
1601 {
1602     int highest = -1;
1603     bool found = false;
1604 
1605     for (const auto& busIface : dbusInterfaceMap)
1606     {
1607         std::string path = busIface.second->get_object_path();
1608         if (std::regex_match(path, std::regex(productName + "(_\\d+|)$")))
1609         {
1610             // Check if the match named has extra information.
1611             found = true;
1612             std::smatch baseMatch;
1613 
1614             bool match = std::regex_match(path, baseMatch,
1615                                           std::regex(productName + "_(\\d+)$"));
1616             if (match)
1617             {
1618                 if (baseMatch.size() == 2)
1619                 {
1620                     std::ssub_match baseSubMatch = baseMatch[1];
1621                     std::string base = baseSubMatch.str();
1622 
1623                     int value = std::stoi(base);
1624                     highest = (value > highest) ? value : highest;
1625                 }
1626             }
1627         }
1628     } // end searching objects
1629 
1630     if (!found)
1631     {
1632         return std::nullopt;
1633     }
1634     return highest;
1635 }
1636 
1637 // This function does format fru data as per IPMI format and find the
1638 // productName in the formatted fru data, get that productName and return
1639 // productName if found or return NULL.
1640 
getProductName(std::vector<uint8_t> & device,boost::container::flat_map<std::string,std::string> & formattedFRU,uint32_t bus,uint32_t address,size_t & unknownBusObjectCount)1641 std::optional<std::string> getProductName(
1642     std::vector<uint8_t>& device,
1643     boost::container::flat_map<std::string, std::string>& formattedFRU,
1644     uint32_t bus, uint32_t address, size_t& unknownBusObjectCount)
1645 {
1646     std::string productName;
1647 
1648     resCodes res = formatIPMIFRU(device, formattedFRU);
1649     if (res == resCodes::resErr)
1650     {
1651         lg2::error("failed to parse FRU for device at bus {BUS} address {ADDR}",
1652                    "BUS", bus, "ADDR", address);
1653         return std::nullopt;
1654     }
1655     if (res == resCodes::resWarn)
1656     {
1657         lg2::error(
1658             "Warnings while parsing FRU for device at bus {BUS} address {ADDR}",
1659             "BUS", bus, "ADDR", address);
1660     }
1661 
1662     auto productNameFind = formattedFRU.find("BOARD_PRODUCT_NAME");
1663     // Not found under Board section or an empty string.
1664     if (productNameFind == formattedFRU.end() ||
1665         productNameFind->second.empty())
1666     {
1667         productNameFind = formattedFRU.find("PRODUCT_PRODUCT_NAME");
1668     }
1669     // Found under Product section and not an empty string.
1670     if (productNameFind != formattedFRU.end() &&
1671         !productNameFind->second.empty())
1672     {
1673         productName = productNameFind->second;
1674         std::regex illegalObject("[^A-Za-z0-9_]");
1675         productName = std::regex_replace(productName, illegalObject, "_");
1676     }
1677     else
1678     {
1679         productName = "UNKNOWN" + std::to_string(unknownBusObjectCount);
1680         unknownBusObjectCount++;
1681     }
1682     return productName;
1683 }
1684 
getFruData(std::vector<uint8_t> & fruData,uint32_t bus,uint32_t address)1685 bool getFruData(std::vector<uint8_t>& fruData, uint32_t bus, uint32_t address)
1686 {
1687     try
1688     {
1689         fruData = getFRUInfo(static_cast<uint16_t>(bus),
1690                              static_cast<uint8_t>(address));
1691     }
1692     catch (const std::invalid_argument& e)
1693     {
1694         lg2::error("Failure getting FRU Info: {ERR}", "ERR", e);
1695         return false;
1696     }
1697 
1698     return !fruData.empty();
1699 }
1700 
isFieldEditable(std::string_view fieldName)1701 bool isFieldEditable(std::string_view fieldName)
1702 {
1703     if (fieldName == "PRODUCT_ASSET_TAG")
1704     {
1705         return true; // PRODUCT_ASSET_TAG is always editable.
1706     }
1707 
1708     if (!ENABLE_FRU_UPDATE_PROPERTY)
1709     {
1710         return false; // If FRU update is disabled, no fields are editable.
1711     }
1712 
1713     // Editable fields
1714     constexpr std::array<std::string_view, 8> editableFields = {
1715         "MANUFACTURER",  "PRODUCT_NAME", "PART_NUMBER",    "VERSION",
1716         "SERIAL_NUMBER", "ASSET_TAG",    "FRU_VERSION_ID", "INFO_AM"};
1717 
1718     // Find position of first underscore
1719     std::size_t pos = fieldName.find('_');
1720     if (pos == std::string_view::npos || pos + 1 >= fieldName.size())
1721     {
1722         return false;
1723     }
1724 
1725     // Extract substring after the underscore
1726     std::string_view subField = fieldName.substr(pos + 1);
1727 
1728     // Trim trailing digits
1729     while (!subField.empty() && (std::isdigit(subField.back()) != 0))
1730     {
1731         subField.remove_suffix(1);
1732     }
1733 
1734     // Match against editable fields
1735     return std::ranges::contains(editableFields, subField);
1736 }
1737 
updateAddProperty(const std::string & propertyValue,const std::string & propertyName,std::vector<uint8_t> & fruData)1738 bool updateAddProperty(const std::string& propertyValue,
1739                        const std::string& propertyName,
1740                        std::vector<uint8_t>& fruData)
1741 {
1742     // Validate field length: must be 2–63 characters
1743     const size_t len = propertyValue.length();
1744     if (len == 1 || len > 63)
1745     {
1746         lg2::error(
1747             "FRU field data must be 0 or between 2 and 63 characters. Invalid Length: {LEN}",
1748             "LEN", len);
1749         return false;
1750     }
1751 
1752     if (fruData.empty())
1753     {
1754         lg2::error("Empty FRU data\n");
1755         return false;
1756     }
1757 
1758     // Extract area name (prefix before underscore)
1759     std::string areaName = propertyName.substr(0, propertyName.find('_'));
1760     auto areaIterator =
1761         std::find(fruAreaNames.begin(), fruAreaNames.end(), areaName);
1762     if (areaIterator == fruAreaNames.end())
1763     {
1764         lg2::error("Failed to get FRU area for property: {AREA}", "AREA",
1765                    areaName);
1766         return false;
1767     }
1768 
1769     fruAreas fruAreaToUpdate = static_cast<fruAreas>(
1770         std::distance(fruAreaNames.begin(), areaIterator));
1771 
1772     std::vector<std::vector<uint8_t>> areasData;
1773     if (!disassembleFruData(fruData, areasData))
1774     {
1775         lg2::error("Failed to disassemble Fru Data");
1776         return false;
1777     }
1778 
1779     std::vector<uint8_t>& areaData =
1780         areasData[static_cast<size_t>(fruAreaToUpdate)];
1781     if (areaData.empty())
1782     {
1783         // If ENABLE_FRU_AREA_RESIZE is not defined then return with failure
1784 #ifndef ENABLE_FRU_AREA_RESIZE
1785         lg2::error(
1786             "FRU area {AREA} not present and ENABLE_FRU_AREA_RESIZE is not set. "
1787             "Returning failure.",
1788             "AREA", areaName);
1789         return false;
1790 #endif
1791         if (!createDummyArea(fruAreaToUpdate, areaData))
1792         {
1793             lg2::error("Failed to create dummy area for {AREA}", "AREA",
1794                        areaName);
1795             return false;
1796         }
1797     }
1798 
1799     if (!setField(fruAreaToUpdate, areaData, propertyName, propertyValue))
1800     {
1801         lg2::error("Failed to set field value for property: {PROPERTY}",
1802                    "PROPERTY", propertyName);
1803         return false;
1804     }
1805 
1806     if (!assembleFruData(fruData, areasData))
1807     {
1808         lg2::error("Failed to reassemble FRU data");
1809         return false;
1810     }
1811 
1812     if (fruData.empty())
1813     {
1814         lg2::error("FRU data is empty after assembly");
1815         return false;
1816     }
1817 
1818     return true;
1819 }
1820