├── .gitignore ├── CMakeLists.txt ├── README.md ├── example.json ├── fru.c ├── fru.h ├── frugen.c └── smbios.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Libraries 12 | *.lib 13 | *.a 14 | *.la 15 | *.lo 16 | 17 | # Shared objects (inc. Windows DLLs) 18 | *.dll 19 | *.so 20 | *.so.* 21 | *.dylib 22 | 23 | # Executables 24 | *.exe 25 | *.out 26 | *.app 27 | *.i*86 28 | *.x86_64 29 | *.hex 30 | 31 | # Debug files 32 | *.dSYM/ 33 | *.su 34 | 35 | 36 | # CMake autogenerated files 37 | CMakeFiles 38 | cmake_install.cmake 39 | CMakeCache.txt 40 | Makefile 41 | 42 | # Targets 43 | frugen 44 | 45 | # Vim swaps 46 | *.swp 47 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project(frugen C) 3 | if(COMMAND cmake_policy) 4 | cmake_policy(SET CMP0003 NEW) 5 | endif(COMMAND cmake_policy) 6 | 7 | execute_process(COMMAND 8 | git 9 | describe 10 | --always 11 | --long 12 | --dirty 13 | WORKING_DIRECTORY 14 | "${CMAKE_CURRENT_SOURCE_DIR}" 15 | RESULT_VARIABLE 16 | res 17 | OUTPUT_VARIABLE 18 | gitver 19 | ERROR_QUIET 20 | OUTPUT_STRIP_TRAILING_WHITESPACE) 21 | 22 | if(NOT res EQUAL 0) 23 | set(gitver "UNKNOWN") 24 | endif() 25 | 26 | string(REPLACE "-" "." gitver "${gitver}") 27 | 28 | option(BINARY_32BIT "compile 32bit version" OFF) 29 | option(DEBUG_OUTPUT "show extra debug output" OFF) 30 | option(DEBUG_SYMBOLS "compile with debug symbols" OFF) 31 | option(DEBUG_NO_OPTIMIZE "do not optimize the code" OFF) 32 | option(COMPILER_WARNINGS "show compiler warnings" ON) 33 | 34 | if (DEBUG_OUTPUT) 35 | add_definitions(-DDEBUG) 36 | endif(DEBUG_OUTPUT) 37 | 38 | add_definitions(-DVERSION="${gitver}") 39 | 40 | function(set_compiler_flags TARGETS) 41 | foreach(TARGET IN LISTS TARGETS) 42 | if ( CMAKE_C_COMPILER_ID MATCHES "Clang|AppleClang|GNU") 43 | target_compile_options(${TARGET} PRIVATE -Wall -Wextra -Wunreachable-code) 44 | endif() 45 | endforeach() 46 | endfunction() 47 | 48 | function(set_compiler_options TARGETS OPTS) 49 | foreach(TARGET IN LISTS TARGETS) 50 | target_compile_options(${TARGET} PRIVATE ${OPTS}) 51 | endforeach() 52 | endfunction() 53 | 54 | add_executable(frugen frugen.c) 55 | add_executable(frugen-static frugen.c) 56 | add_library(fru-static STATIC fru.c) 57 | add_library(fru-shared SHARED fru.c) 58 | find_library(JSON_LIB json-c) 59 | SET_TARGET_PROPERTIES(fru-static PROPERTIES OUTPUT_NAME fru CLEAN_DIRECT_OUTPUT 1) 60 | SET_TARGET_PROPERTIES(fru-shared PROPERTIES OUTPUT_NAME fru CLEAN_DIRECT_OUTPUT 1) 61 | target_link_libraries(frugen fru-static) 62 | target_link_libraries(frugen-static fru-static -static) 63 | if (JSON_LIB) 64 | message (STATUS "Using JSON Library found at " ${JSON_LIB}) 65 | add_definitions(-D__HAS_JSON__) 66 | target_link_libraries(frugen ${JSON_LIB}) 67 | target_link_libraries(frugen-static ${JSON_LIB} -static) 68 | else (JSON_LIB) 69 | message (WARNING "JSON library *NOT* found. JSON support *disabled*!") 70 | endif (JSON_LIB) 71 | 72 | if (COMPILER_WARNINGS) 73 | set_compiler_flags("frugen;frugen-static;fru-static;fru-shared") 74 | endif(COMPILER_WARNINGS) 75 | 76 | # To make frugen 32-bit, uncomment the following lines or use an external toolchain file 77 | if (BINARY_32BIT) 78 | set_target_properties(frugen PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32") 79 | set_target_properties(frugen-static PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32") 80 | set_target_properties(fru-static PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32") 81 | set_target_properties(fru-shared PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32") 82 | endif (BINARY_32BIT) 83 | 84 | # The following are for debugging only 85 | if (DEBUG_SYMBOLS) 86 | set_compiler_options("frugen;frugen-static;fru-static;fru-shared" "-g3") 87 | endif (DEBUG_SYMBOLS) 88 | if (DEBUG_NO_OPTIMIZE) 89 | set_compiler_options("frugen;frugen-static;fru-static;fru-shared" "-O0") 90 | endif (DEBUG_NO_OPTIMIZE) 91 | # target_compile_definitions(frugen PUBLIC __STANDALONE__) 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frugen / libfru 2 | 3 | ## License 4 | 5 | This work is dual-licensed under Apache 2.0 and GPL 2.0 (or any later version) 6 | for the frugen utility, or under Apache 2.0 and Lesser GPL 2.0 (or any later version) 7 | for the fru library. 8 | 9 | You can choose between one of them if you use this work. 10 | 11 | `SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later` 12 | 13 | ## Introduction 14 | 15 | This project was incepted to eventually create a universal, full-featured IPMI 16 | FRU Information generator / editor library and command line tool, written in 17 | full compliance with IPMI FRU Information Storage Definition v1.0, rev. 1.3., see 18 | http://www.intel.com/content/www/us/en/servers/ipmi/ipmi-platform-mgt-fru-infostorage-def-v1-0-rev-1-3-spec-update.html 19 | 20 | ## libfru 21 | 22 | So far supported in libfru: 23 | 24 | * Data encoding into all the defined formats (binary, BCD plus, 6-bit ASCII, language code specific text). 25 | The exceptions are: 26 | 27 | * all text is always encoded as if the language code was English (ASCII, 1 byte per character) 28 | * encoding is selected automatically based on value range of the supplied data, only binary format 29 | can be enforced by specifying the length other than LEN_AUTO. 30 | 31 | * Data decoding from all the declared formats. 32 | Exception: Unicode is not supported 33 | 34 | * Internal use area creation, with the following limitations: 35 | 36 | * Only from file 37 | * Only automatic sizing, all data must be specified in 38 | the input template file 39 | 40 | * Chassis information area creation 41 | * Board information area creation 42 | * Product information area creation 43 | * Multirecord area creation with the following record types: 44 | 45 | * Management Access Record with the following subtypes: 46 | 47 | * System UUID 48 | 49 | * FRU file creation (in a memory buffer) 50 | 51 | NOT supported: 52 | 53 | * Internal use area creation/modification from the command line 54 | * Miltirecord area record types other than listed above 55 | 56 | ## frugen 57 | 58 | The frugen tool supports the following (limitations imposed by the libfru library): 59 | 60 | * Board area creation (including custom fields) 61 | * Product area creation (including custom fields) 62 | * Chassis area creation (including custom fields) 63 | * Multirecord area creation (see libfru supported types above) 64 | 65 | The limitations: 66 | 67 | * All data fields (except custom) are always treated as ASCII text, and the encoding 68 | is automatically selected based on the byte range of the provided data. Custom fields 69 | may be forced to be binary using --binary option. 70 | * Internal use area is not supported 71 | * You should specify the UUID and the custom fields in either 72 | the template file OR in the command line. The command line does 73 | NOT override the template file in that regard, but creates an additional 74 | UUID or custom record, which may be undesirable 75 | 76 | For the most up-to-date information on the frugen tool invocation and options, please 77 | use `frugen -h`, below is an example of the output of that command: 78 | 79 | ``` 80 | FRU Generator v1.3.2.g1429f89 (c) 2016-2021, Alexander Amelkin 81 | 82 | Usage: frugen [options] 83 | 84 | Options: 85 | 86 | -h, --help 87 | Display this help. 88 | 89 | -v, --verbose 90 | Increase program verbosity (debug) level. 91 | 92 | -b, --binary 93 | Mark the next --*-custom option's argument as binary. 94 | Use hex string representation for the next custom argument. 95 | 96 | Example: frugen --binary --board-custom 0012DEADBEAF 97 | 98 | There must be an even number of characters in a 'binary' argument. 99 | 100 | -I, --ascii 101 | Disable auto-encoding on all fields, force ASCII. 102 | Out of ASCII range data will still result in binary encoding. 103 | 104 | -j, --json 105 | Set input text file format to JSON (default). Specify before '--from'. 106 | 107 | -z, --from 108 | Load FRU information from a text file. 109 | 110 | -t, --chassis-type 111 | Set chassis type (hex). Defaults to 0x02 ('Unknown'). 112 | 113 | -a, --chassis-pn 114 | Set chassis part number. 115 | 116 | -c, --chassis-serial 117 | Set chassis serial number. 118 | 119 | -C, --chassis-custom 120 | Add a custom chassis information field, may be used multiple times. 121 | 122 | -n, --board-pname 123 | Set board product name. 124 | 125 | -m, --board-mfg 126 | Set board manufacturer name. 127 | 128 | -d, --board-date 129 | Set board manufacturing date/time, use "DD/MM/YYYY HH:MM:SS" format. 130 | By default the current system date/time is used unless -u is specified. 131 | 132 | -u, --board-date-unspec 133 | Don't use current system date/time for board mfg. date, use 'Unspecified'. 134 | 135 | -p, --board-pn 136 | Set board part number. 137 | 138 | -s, --board-serial 139 | Set board serial number. 140 | 141 | -f, --board-file 142 | Set board FRU file ID. 143 | 144 | -B, --board-custom 145 | Add a custom board information field, may be used multiple times. 146 | 147 | -N, --prod-name 148 | Set product name. 149 | 150 | -G, --prod-mfg 151 | Set product manufacturer name. 152 | 153 | -M, --prod-modelpn 154 | Set product model / part number. 155 | 156 | -V, --prod-version 157 | Set product version. 158 | 159 | -S, --prod-serial 160 | Set product serial number. 161 | 162 | -F, --prod-file 163 | Set product FRU file ID. 164 | 165 | -A, --prod-atag 166 | Set product Asset Tag. 167 | 168 | -P, --prod-custom 169 | Add a custom product information field, may be used multiple times. 170 | 171 | -U, --mr-uuid 172 | Set System Unique ID (UUID/GUID). 173 | 174 | Example: 175 | frugen --board-mfg "Biggest International Corp." \ 176 | --board-pname "Some Cool Product" \ 177 | --board-pn "BRD-PN-123" \ 178 | --board-date "10/1/2017 12:58:00" \ 179 | --board-serial "01171234" \ 180 | --board-file "Command Line" \ 181 | --binary --board-custom "01020304FEAD1E" \ 182 | fru.bin 183 | ``` 184 | 185 | ### JSON 186 | 187 | __Dependency:__ json-c library (https://github.com/json-c/json-c) 188 | 189 | The frugen tool supports JSON files. You may specify all the FRU info fields (mind the 190 | general tool limitations) in a file and use it as an input for the tool: 191 | 192 | frugen --json --from=example.json fru.bin 193 | 194 | An example file 'example.json' is provided for your reference. 195 | 196 | NOTE: The JSON file for frugen is allowed to have C-style comments (`/* comment */`), 197 | which is an extension to the standard JSON format. 198 | 199 | ## Building 200 | 201 | ### Linux 202 | 203 | cmake . 204 | make 205 | 206 | ### Windows (cross-compiled on Linux) 207 | 208 | You will need a MingW32 toolchain. This chapter is written in assumption you're 209 | using x86\_64-w64-mingw32. 210 | 211 | First of all you will need to create a x86\_64-w64-mingw32-toolchain.cmake 212 | file describing your cross-compilation toolchain. 213 | 214 | This file assumes that you use $HOME/mingw-install as an installation prefix 215 | for all mingw32-compiled libraries (e.g., libjson-c). 216 | 217 | # the name of the target operating system 218 | SET(CMAKE_SYSTEM_NAME Windows) 219 | 220 | SET(MINGW32_INSTALL_DIR $ENV{HOME}/mingw-install) 221 | 222 | # which compilers to use for C and C++ 223 | SET(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) 224 | SET(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) 225 | SET(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) 226 | 227 | # here is the target environment located 228 | SET(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32/ ${MINGW32_INSTALL_DIR}) 229 | 230 | # adjust the default behaviour of the FIND_XXX() commands: 231 | # search headers and libraries in the target environment, search 232 | # programs in the host environment 233 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 234 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 235 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 236 | 237 | # let the compiler find the includes and linker find the libraries 238 | # MinGW linker doesn't accept absolute library paths found 239 | # by find_library() 240 | include_directories(BEFORE ${MINGW32_INSTALL_DIR}/include) 241 | set(CMAKE_LIBRARY_PATH ${MINGW32_INSTALL_DIR}/lib ${MINGW32_INSTALL_DIR}/usr/lib ${CMAKE_LIBRARY_PATH}) 242 | 243 | Once you have that file, build the tool as follows: 244 | 245 | cmake -DCMAKE_TOOLCHAIN_FILE=x86\_64-w64-mingw32-toolchain.cmake . 246 | make 247 | 248 | ## Contact information 249 | 250 | Should you have any questions or proposals, please feel free to: 251 | 252 | * Ask a question via https://github.com/ipmitool/frugen/discussions 253 | * Submit changes as a pull request via https://github.com/ipmitool/frugen/pulls 254 | * Report a problem by creating an issue at https://github.com/ipmitool/frugen/issues 255 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | /* The internal use area, if specified, must list all the data as hex string, 3 | the length is calculated automatically */ 4 | "internal" : "010203040A0B0C0D", 5 | "chassis" : { 6 | "type": 10, 7 | "pn" : "CHAS-C00L-12", 8 | /* Fields may be given explicit types by setting to object with keys `type` and `data`*/ 9 | /* Supported types are: "bcdplus", "6bitascii" and "text" */ 10 | "serial": { 11 | "type": "bcdplus", 12 | "data": "45678" 13 | }, 14 | "custom" : [ 15 | { "data" : "Auto-typed text custom field" }, 16 | { "type" : "binary", "data": "B14A87" }, 17 | /* For explicit text types range is not checked, so be careful */ 18 | { "type" : "bcdplus", "data": "1234" }, 19 | { "type" : "6bitascii", "data": "1234" }, 20 | { "type" : "text", "data": "1234" } 21 | ] 22 | }, 23 | "board" : { 24 | /* The date, if not specified, will be taken automatically from 25 | * the current system time. You may specify the `-u` option to 26 | * `frugen` in order to leave the date 'Unspecified' */ 27 | /* "date" : "1/10/2016 3:00:45",*/ 28 | "mfg" : "Biggest International Corp.", 29 | "pname" : "Some Cool Product", 30 | "serial" : "123456", 31 | "pn" : "BRD-PN-345", 32 | "file" : "example1.json", 33 | "custom" : [ 34 | { "type" : "binary", "data" : "0123DEADBABE" }, 35 | { "type" : "auto", "data" : "This is a text custom field" }, 36 | { "type" : "auto", "data" : "This is test2" } 37 | ] 38 | }, 39 | "product" : { 40 | "lang": 1, 41 | "mfg" : "Super OEM Company", 42 | "pn" : "PRD-PN-1234", 43 | "pname" : "Label-engineered Super Product", 44 | "serial" : "OEM12345", 45 | "atag" : "Accounting Dept.", 46 | "ver" : "v1.1", 47 | "file" : "example2.json", 48 | "custom" : [ 49 | { "type" : "auto", "data" : "Product Custom 1" }, 50 | { "type" : "auto", "data" : "PRDCSTM" }, 51 | { "type" : "auto", "data" : "PRDCSTM2" }, 52 | { "type" : "binary", "data" : "C001BEEF" } 53 | ] 54 | }, 55 | "multirecord" : [ 56 | { "type" : "management", "subtype" : "uuid", "uuid" : "9bd70799-ccf0-4915-a7f9-7ce7d64385cf" } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /fru.c: -------------------------------------------------------------------------------- 1 | /** @file 2 | * @brief FRU information encoding functions 3 | * 4 | * Copyright (C) 2016-2021 Alexander Amelkin 5 | * SPDX-License-Identifier: LGPL-2.0-or-later OR Apache-2.0 6 | */ 7 | 8 | #include "fru.h" 9 | #include "smbios.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #define _BSD_SOURCE 26 | #include 27 | 28 | #ifdef __STANDALONE__ 29 | #include 30 | #endif 31 | 32 | #ifdef DEBUG 33 | #undef DEBUG 34 | #include 35 | #define DEBUG(f, args...) do { printf("%s:%d: ", __func__, __LINE__); printf(f,##args); } while(0) 36 | #else 37 | #define DEBUG(f, args...) 38 | #endif 39 | 40 | static bool autodetect = true; 41 | 42 | void fru_set_autodetect(bool enable) 43 | { 44 | autodetect = enable; 45 | } 46 | 47 | /** 48 | * Strip trailing spaces 49 | */ 50 | static inline void cut_tail(char *s) 51 | { 52 | int i; 53 | for(i = strlen(s) - 1; i >= 0 && ' ' == s[i]; i--) s[i] = 0; 54 | } 55 | 56 | /** Copy a FRU area field to a buffer and return the field's size */ 57 | static inline uint8_t fru_field_copy(void *dest, const fru_field_t *fieldp) 58 | { 59 | memcpy(dest, (void *)fieldp, FRU_FIELDSIZE(fieldp->typelen)); 60 | return FRU_FIELDSIZE(fieldp->typelen); 61 | } 62 | 63 | /** 64 | * Detect the most suitable encoding for the string and calculate the length as well 65 | * 66 | * @returns A FRU field type/length byte, as per IPMI FRU Storage Definition, if everything was ok, or an error code. 67 | * @retval FRU_FIELD_EMPTY The \a data argument was NULL 68 | * @retval FRU_FIELD_TERMINATOR The data exceeded the maximum length (63 bytes) 69 | * 70 | */ 71 | static 72 | uint8_t fru_get_typelen(int len, /**< [in] Length of the data, 73 | LEN_AUTO for pure text zero-terminated data or 74 | one of LEN_BCDPLUS, LEN_6BITASCII, LEN_TEXT for explicit text type */ 75 | const uint8_t *data) /**< [in] The input data */ 76 | { 77 | uint8_t typelen = len; 78 | int i; 79 | 80 | if (!data) 81 | return FRU_FIELD_EMPTY; 82 | 83 | if (len < 0) { 84 | DEBUG("Forcing string '%s' to ...\n", (char *)data); 85 | // Explicit text type 86 | if (len == LEN_BCDPLUS) { 87 | DEBUG("BCDPLUS type\n"); 88 | return FRU_TYPELEN(BCDPLUS, (strlen(data) + 1) / 2); 89 | } else if (len == LEN_6BITASCII) { 90 | DEBUG("6BIT ASCII type\n"); 91 | return FRU_TYPELEN(ASCII_6BIT, FRU_6BIT_LENGTH(strlen(data))); 92 | } else if (len == LEN_TEXT) { 93 | DEBUG("ASCII type\n"); 94 | return FRU_TYPELEN(TEXT, strlen(data)); 95 | } else { 96 | DEBUG("Nothing... Unknown text type\n"); 97 | return FRU_FIELD_TERMINATOR; 98 | } 99 | } 100 | 101 | if (!len) { 102 | len = strlen(data); 103 | if (!len) { 104 | return FRU_FIELD_EMPTY; 105 | } 106 | } 107 | 108 | // If the data exceeds the maximum length, return a terminator 109 | if (len > FRU_FIELDDATALEN(len)) { 110 | DEBUG("Data exceeds maximum length\n"); 111 | return FRU_FIELD_TERMINATOR; 112 | } 113 | 114 | if (typelen) { 115 | // They gave us a non-zero length. The data must be binary. Trust 'em and don't try to optimize. 116 | DEBUG("Binary data due to non-zero length\n"); 117 | return FRU_TYPELEN(BINARY, len); 118 | } 119 | 120 | DEBUG("Guessing type of string '%s'...\n", (char *)data); 121 | 122 | // As we reach this point, we know the data must be text. 123 | // We will try to find the encoding that suits best. 124 | if (autodetect) { 125 | typelen = FRU_TYPELEN(BCDPLUS, (len + 1) / 2); // By default - the most range-restricted text type 126 | 127 | DEBUG("Assuming BCD plus data...\n"); 128 | } 129 | else { 130 | DEBUG("Assuming ASCII data...\n"); 131 | typelen = FRU_TYPELEN(TEXT, len); 132 | } 133 | 134 | // Go through the data and expand charset as needed 135 | for (i = 0; i < len; i++) { 136 | if (data[i] < ' ' 137 | && data[i] != '\t' 138 | && data[i] != '\r' 139 | && data[i] != '\n') 140 | { 141 | // They lied! The data is binary! 142 | // That's the widest range type. 143 | // There is no use in checking any further. 144 | DEBUG("[%#02x] Binary data!\n", data[i]); 145 | typelen = FRU_TYPELEN(BINARY, len); 146 | break; 147 | } 148 | 149 | if (autodetect) { 150 | if (typelen < FRU_MAKETYPE(TEXT) 151 | && (data[i] > '_' || data[i] < ' ')) 152 | { // Do not reduce the range 153 | // The data doesn't fit into 6-bit ASCII, expand to simple text. 154 | DEBUG("[%c] Data is simple text!\n", data[i]); 155 | typelen = FRU_TYPELEN(TEXT, len); 156 | continue; 157 | } 158 | 159 | if (typelen < FRU_MAKETYPE(ASCII_6BIT) && // Do not reduce the range 160 | !isdigit(data[i]) && data[i] != ' ' && data[i] != '-' && data[i] != '.') 161 | { 162 | // The data doesn't fit into BCD plus, expand to 163 | DEBUG("[%c] Data is 6-bit ASCII!\n", data[i]); 164 | typelen = FRU_TYPELEN(ASCII_6BIT, FRU_6BIT_LENGTH(len)); 165 | } 166 | } /* autodetect */ 167 | } 168 | 169 | return typelen; 170 | } 171 | 172 | /** 173 | * Allocate a buffer and encode the input string into it as 6-bit ASCII 174 | * 175 | * @returns pointer to the newly allocated field buffer if allocation and encoding were successful 176 | * @returns NULL if there was an error, sets errno accordingly (man malloc) 177 | */ 178 | static fru_field_t *fru_encode_6bit(const unsigned char *s /**< [in] Input string */) 179 | { 180 | int len = strlen(s); 181 | int len6bit = FRU_6BIT_LENGTH(len); 182 | int i, i6; 183 | fru_field_t *out = NULL; 184 | size_t outlen = sizeof(fru_field_t) + len6bit + 1; // 1 extra for null-byte 185 | 186 | if (len6bit > FRU_FIELDDATALEN(len6bit) || 187 | !(out = calloc(1, outlen))) 188 | { 189 | return out; 190 | } 191 | 192 | out->typelen = FRU_TYPELEN(ASCII_6BIT, len6bit); 193 | 194 | for (i = 0, i6 = 0; i < len && i6 < len6bit; i++) { 195 | int base = i / 4; // Four original bytes get encoded into three 6-bit-packed ones 196 | int byte = i % 4; 197 | char c = (s[i] - ' ') & 0x3F; // Space is zero, maximum is 0x3F (6 significant bits) 198 | 199 | DEBUG("%d:%d:%d = %c -> %02hhX\n", base, byte, i6, s[i], c); 200 | switch(byte) { 201 | case 0: 202 | out->data[i6] = c; 203 | break; 204 | case 1: 205 | out->data[i6] |= (c & 0x03) << 6; // Lower 2 bits go high into byte 0 206 | out->data[++i6] = c >> 2; // Higher (4) bits go low into byte 1 207 | break; 208 | case 2: 209 | out->data[i6++] |= c << 4; // Lower 4 bits go high into byte 1 210 | out->data[i6] = c >> 4; // Higher 2 bits go low into byte 2 211 | break; 212 | case 3: 213 | out->data[i6++] |= c << 2; // The whole 6-bit char goes high into byte 3 214 | break; 215 | } 216 | } 217 | 218 | return out; 219 | } 220 | 221 | /** 222 | * Allocate a buffer and decode a 6-bit ASCII string from it 223 | */ 224 | static unsigned char *fru_decode_6bit(const fru_field_t *field) 225 | { 226 | unsigned char *out = NULL; 227 | const unsigned char *s6; 228 | int len, len6bit; 229 | int i, i6; 230 | 231 | if (!field) return out; 232 | 233 | len6bit = FRU_FIELDDATALEN(field->typelen); 234 | s6 = field->data; 235 | 236 | len = FRU_6BIT_FULLLENGTH(len6bit); 237 | if (!(out = calloc(1, len + 1))) { 238 | return out; 239 | } 240 | DEBUG("Allocated a destination buffer at %p\n", out); 241 | 242 | for(i = 0, i6 = 0; i6 <= len6bit && i < len && s6[i6]; i++) { 243 | int base = i / 4; 244 | int byte = i % 4; 245 | 246 | DEBUG("%d:%d:%d = ", base, byte, i6); 247 | 248 | switch(byte) { 249 | case 0: 250 | DEBUG("%02hhX ", s6[i6]); 251 | out[i] = s6[i6] & 0x3F; 252 | break; 253 | case 1: 254 | DEBUG("%02hhX %02hhX ", s6[i6], s6[i6 + 1]); 255 | out[i] = (s6[i6] >> 6) | (s6[++i6] << 2); 256 | break; 257 | case 2: 258 | DEBUG("%02hhX %02hhX ", s6[i6], s6[i6 + 1]); 259 | out[i] = (s6[i6] >> 4) | (s6[++i6] << 4); 260 | break; 261 | case 3: 262 | DEBUG("%02hhX ", s6[i6]); 263 | out[i] = s6[i6++] >> 2; 264 | break; 265 | } 266 | out[i] &= 0x3F; 267 | out[i] += ' '; 268 | DEBUG("-> %02hhx %c\n", out[i], out[i]); 269 | } 270 | 271 | // Strip trailing spaces that could emerge when decoding a 272 | // string that was a byte shorter than a multiple of 4. 273 | cut_tail(out); 274 | 275 | return out; 276 | } 277 | 278 | /** 279 | * Allocate a buffer and encode that data as per FRU specification 280 | */ 281 | fru_field_t * fru_encode_data(int len, const uint8_t *data) 282 | { 283 | int typelen; 284 | fru_field_t *out; 285 | 286 | typelen = fru_get_typelen(len, data); 287 | if (FRU_FIELD_TERMINATOR == typelen) 288 | return NULL; // Can't encode this data 289 | 290 | if (FRU_ISTYPE(typelen, ASCII_6BIT)) { 291 | out = fru_encode_6bit(data); 292 | } 293 | else { 294 | if (!(out = malloc(FRU_FIELDSIZE(typelen) + 1))) // Plus 1 byte for null-terminator 295 | return NULL; 296 | 297 | out->typelen = typelen; 298 | if (FRU_ISTYPE(typelen, BCDPLUS)) { 299 | int i; 300 | uint8_t c[2] = {0}; 301 | 302 | /* Copy the data and pack it as BCD */ 303 | for (i = 0; i < 2 * FRU_FIELDDATALEN(typelen); i++) { 304 | switch(data[i]) { 305 | case 0: // The null-terminator encountered earlier than end of BCD field, encode as space 306 | case ' ': 307 | c[i % 2] = 0xA; 308 | break; 309 | case '-': 310 | c[i % 2] = 0xB; 311 | break; 312 | case '.': 313 | c[i % 2] = 0xC; 314 | break; 315 | default: // Digits 316 | c[i % 2] = data[i] - '0'; 317 | } 318 | out->data[i / 2] = c[0] << 4 | c[1]; 319 | } 320 | } 321 | else { 322 | memcpy(out->data, data, FRU_FIELDDATALEN(typelen)); 323 | } 324 | out->data[FRU_FIELDDATALEN(typelen)] = 0; // Terminate the string (for safety) 325 | } 326 | 327 | return out; 328 | } 329 | 330 | /** 331 | * Allocate a buffer and decode the data from it. 332 | * 333 | * For binary data use FRU_FIELDDATALEN(field->typelen) to find 334 | * out the size of the returned buffer. 335 | */ 336 | static 337 | unsigned char * fru_decode_data(const fru_field_t *field) 338 | { 339 | unsigned char * out; 340 | 341 | if (!field) return NULL; 342 | 343 | if (FRU_ISTYPE(field->typelen, ASCII_6BIT)) { 344 | out = fru_decode_6bit(field); 345 | } 346 | else { 347 | out = malloc(FRU_FIELDDATALEN(field->typelen) + 1); 348 | if (!out) return NULL; 349 | 350 | if (FRU_ISTYPE(field->typelen, BCDPLUS)) { 351 | int i; 352 | uint8_t c; 353 | /* Copy the data and pack it as BCD */ 354 | for (i = 0; i < 2 * FRU_FIELDDATALEN(field->typelen); i++) { 355 | c = (field->data[i / 2] >> ((i % 2) ? 0 : 4)) & 0x0F; 356 | switch(c) { 357 | case 0xA: 358 | out[i] = ' '; 359 | break; 360 | case 0xB: 361 | out[i] = '-'; 362 | break; 363 | case 0xC: 364 | out[i] = '.'; 365 | break; 366 | default: // Digits 367 | out[i] = c + '0'; 368 | } 369 | } 370 | out[2 * FRU_FIELDDATALEN(field->typelen)] = 0; // Terminate the string 371 | // Strip trailing spaces that may have emerged when a string of odd 372 | // length was BCD-encoded. 373 | cut_tail(out); 374 | } 375 | else { 376 | memcpy(out, field->data, FRU_FIELDDATALEN(field->typelen)); 377 | out[FRU_FIELDDATALEN(field->typelen)] = 0; // Terminate the string 378 | } 379 | } 380 | 381 | return out; 382 | } 383 | 384 | #if 0 385 | struct timeval { 386 | time_t tv_sec; /* seconds */ 387 | suseconds_t tv_usec; /* microseconds */ 388 | }; 389 | 390 | struct timezone { 391 | int tz_minuteswest; /* minutes west of Greenwich */ 392 | int tz_dsttime; /* type of DST correction */ 393 | }; 394 | 395 | gettimeofday time 396 | #endif 397 | 398 | /** 399 | * Calculate zero checksum for command header and FRU areas 400 | */ 401 | static 402 | uint8_t calc_checksum(void *blk, size_t blk_bytes) 403 | { 404 | if (!blk || blk_bytes == 0) { 405 | printf("Null pointer or zero buffer length\n"); 406 | exit(1); 407 | } 408 | 409 | uint8_t *data = (uint8_t *)blk; 410 | uint8_t checksum = 0; 411 | 412 | for(int i = 0; i < blk_bytes; i++) { 413 | checksum += data[i]; 414 | } 415 | 416 | return (uint8_t)( -(int8_t)checksum); 417 | } 418 | 419 | /** 420 | * Calculate an area checksum 421 | * 422 | * Calculation includes the checksum byte itself. 423 | * For freshly prepared area this method returns a checksum to be stored in the last byte. 424 | * For a pre-existing area this method returns zero if checksum is ok or non-zero otherwise. 425 | * 426 | */ 427 | uint8_t fru_area_checksum(fru_info_area_t *area) 428 | { 429 | return calc_checksum(area, (area->blocks * FRU_BLOCK_SZ)); 430 | } 431 | 432 | /** 433 | * Allocate and build a FRU Information Area block of any type. 434 | * 435 | * The function will allocate a buffer of size that is a muliple of 8 bytes 436 | * and is big enough to accomodate the standard area header corresponding to the 437 | * requested area type, as well as all the supplied data fields, the require padding, 438 | * and a checksum byte. 439 | * 440 | * The data fields will be taken as is and should be supplied pre-encoded in 441 | * the standard FRU field format. 442 | * 443 | * It is safe to free (deallocate) the fields supplied to this function 444 | * immediately after the call as all the data is copied to the new buffer. 445 | * 446 | * Don't forget to free() the returned buffer when you don't need it anymore. 447 | * 448 | * @returns fru_info_area_t *area A newly allocated buffer containing the created area 449 | * 450 | */ 451 | static 452 | fru_info_area_t *fru_create_info_area(fru_area_type_t atype, ///< [in] Area type (FRU_[CHASSIS|BOARD|PRODUCT]_INFO) 453 | uint8_t langtype, ///< [in] Language code for areas that use it (board, product) or Chassis Type for chassis info area 454 | const struct timeval *tv, ///< [in] Manufacturing time since the Epoch (1970/01/01 00:00:00 +0000 UTC) for areas that use it (board) 455 | fru_reclist_t *fields, ///< [in] Single-linked list of data fields 456 | size_t nstrings, ///< [in] Number of strings for mandatory fields 457 | const typed_field_t strings[]) ///<[in] Array of typed strings for mandatory fields 458 | { 459 | int i = 0; 460 | int field_count; 461 | int typelen; 462 | int padding_size; 463 | fru_board_area_t header = { // Allocate the biggest possible header 464 | .ver = FRU_VER_1, 465 | }; 466 | int headerlen = FRU_INFO_AREA_HEADER_SZ; // Assume a smallest possible header for a generic info area 467 | void *out = NULL; 468 | uint8_t *outp; 469 | fru_reclist_t *field = fields; 470 | int totalsize = 2; // A generic info area has a custom fields terminator and a checksum 471 | 472 | if (!FRU_AREA_IS_GENERIC(atype)) { 473 | errno = EINVAL; // This function doesn't support multirecord or internal use areas 474 | goto err; 475 | } 476 | 477 | header.langtype = langtype; 478 | 479 | if (FRU_AREA_HAS_DATE(atype)) { 480 | uint32_t fru_time; 481 | struct tm tm_1996 = { 482 | .tm_year = 96, 483 | .tm_mon = 0, 484 | .tm_mday = 1 485 | }; 486 | const struct timeval tv_unspecified = { 0 }; 487 | struct timeval tv_1996 = { 0 }; 488 | 489 | if (!tv) { 490 | errno = EFAULT; 491 | goto err; 492 | } 493 | 494 | /* 495 | * It's assumed here that UNIX time 0 (Jan 1st of 1970) 496 | * can never actually happen in a FRU file in 2018. 497 | */ 498 | if (!memcmp(&tv_unspecified, tv, sizeof(tv))) { 499 | printf("Using FRU_DATE_UNSPECIFIED\n"); 500 | fru_time = FRU_DATE_UNSPECIFIED; 501 | } else { 502 | // The argument to mktime is zoneless 503 | tv_1996.tv_sec = mktime(&tm_1996); 504 | // FRU time is in minutes and we don't care about microseconds 505 | fru_time = (tv->tv_sec - tv_1996.tv_sec) / 60; 506 | } 507 | header.mfgdate[0] = fru_time & 0xFF; 508 | header.mfgdate[1] = (fru_time >> 8) & 0xFF; 509 | header.mfgdate[2] = (fru_time >> 16) & 0xFF; 510 | headerlen = FRU_DATE_AREA_HEADER_SZ; // Expand the header size 511 | } 512 | 513 | DEBUG("headerlen is %d\n", headerlen); 514 | 515 | totalsize += headerlen; 516 | 517 | /* Find uninitialized mandatory fields, allocate and initialize them with provided strings */ 518 | for (field_count = 0, field = fields; 519 | field && !field->rec && field_count < nstrings; 520 | field = field->next, field_count++) 521 | { 522 | int len = LEN_AUTO; 523 | switch (strings[field_count].type) { 524 | case FIELD_TYPE_BINARY: { 525 | DEBUG("binary format not yet implemented\n"); 526 | errno = EINVAL; 527 | goto err; 528 | } 529 | case FIELD_TYPE_BCDPLUS: { 530 | len = LEN_BCDPLUS; 531 | break; 532 | } 533 | case FIELD_TYPE_SIXBITASCII: { 534 | len = LEN_6BITASCII; 535 | break; 536 | } 537 | case FIELD_TYPE_TEXT: { 538 | len = LEN_TEXT; 539 | break; 540 | } 541 | default: 542 | len = LEN_AUTO; 543 | break; 544 | } 545 | field->rec = fru_encode_data(len, strings[field_count].val); 546 | if (!field->rec) goto err; 547 | } 548 | 549 | /* Now calculate the total size of all initialized (mandatory and custom) fields */ 550 | for (field = &fields[0]; field && field->rec; field = field->next) { 551 | totalsize += FRU_FIELDSIZE(field->rec->typelen); 552 | } 553 | 554 | header.blocks = FRU_BLOCKS(totalsize); // Round up to multiple of 8 bytes 555 | padding_size = header.blocks * FRU_BLOCK_SZ - totalsize; 556 | 557 | out = calloc(1, FRU_BYTES(header.blocks)); // This will be returned and freed by the caller 558 | outp = out; 559 | 560 | if (!out) goto err; 561 | 562 | // Now fill the output buffer. First copy the header. 563 | memcpy(outp, &header, headerlen); 564 | outp += headerlen; 565 | 566 | DEBUG("area size is %d (%d) bytes\n", totalsize, FRU_BYTES(header.blocks)); 567 | DEBUG("area size in header is (%d) bytes\n", FRU_BYTES(((fru_info_area_t *)out)->blocks)); 568 | 569 | // Add the data fields 570 | for (field = fields; field && field->rec; field = field->next) { 571 | outp += fru_field_copy(outp, field->rec); 572 | } 573 | 574 | // Terminate the data fields, add padding and checksum 575 | *outp = FRU_FIELD_TERMINATOR; 576 | outp += 1 + padding_size; 577 | *outp = fru_area_checksum(out); 578 | 579 | DEBUG("area size is %d (%d) bytes\n", totalsize, FRU_BYTES(header.blocks)); 580 | DEBUG("area size in header is (%d) bytes\n", FRU_BYTES(((fru_info_area_t *)out)->blocks)); 581 | 582 | err: 583 | /* 584 | * Free the allocated mandatory fields. Either an error has occured or the fields 585 | * have already been copied into the output buffer. Anyway, they aren't needed anymore 586 | */ 587 | for (--field_count; field_count >= 0; field_count--) { 588 | free(fields[field_count].rec); 589 | } 590 | return out; 591 | } 592 | 593 | /** 594 | * Allocate and build a Chassis Information Area block. 595 | * 596 | * The function will allocate a buffer of size that is a muliple of 8 bytes 597 | * and is big enough to accomodate the standard area header, all the mandatory 598 | * fields, all the supplied custom fields, the required padding and a checksum byte. 599 | * 600 | * The mandatory fields will be encoded as fits best. 601 | * The custom fields will be used as is (pre-encoded). 602 | * 603 | * It is safe to free (deallocate) any arguments supplied to this function 604 | * immediately after the call as all the data is copied to the new buffer. 605 | * 606 | * Don't forget to free() the returned buffer when you don't need it anymore. 607 | * 608 | * @returns fru_info_area_t *area A newly allocated buffer containing the created area 609 | * 610 | */ 611 | fru_chassis_area_t * fru_chassis_info(const fru_exploded_chassis_t *chassis) ///< [in] Exploded chassis info area 612 | { 613 | int i; 614 | 615 | if(!chassis) { 616 | errno = EFAULT; 617 | return NULL; 618 | } 619 | 620 | fru_reclist_t fields[] = { // List of fields. Mandatory fields are unallocated yet. 621 | [FRU_CHASSIS_PARTNO] = { NULL, &fields[FRU_CHASSIS_SERIAL] }, 622 | [FRU_CHASSIS_SERIAL] = { NULL, chassis->cust }, 623 | }; 624 | 625 | const typed_field_t strings[] = { chassis->pn, chassis->serial }; 626 | fru_chassis_area_t *out = NULL; 627 | 628 | if (!SMBIOS_CHASSIS_IS_VALID(chassis->type)) { 629 | errno = EINVAL; 630 | return NULL; 631 | } 632 | 633 | out = fru_create_info_area(FRU_CHASSIS_INFO, 634 | chassis->type, NULL, fields, 635 | ARRAY_SZ(strings), strings); 636 | 637 | return out; 638 | } 639 | 640 | /** 641 | * Allocate and build a Board Information Area block. 642 | * 643 | * The function will allocate a buffer of size that is a muliple of 8 bytes 644 | * and is big enough to accomodate the standard area header, all the mandatory 645 | * fields, all the supplied custom fields, the required padding and a checksum byte. 646 | * 647 | * The mandatory fields will be encoded as fits best. 648 | * The custom fields will be used as is (pre-encoded). 649 | * 650 | * It is safe to free (deallocate) any arguments supplied to this function 651 | * immediately after the call as all the data is copied to the new buffer. 652 | * 653 | * Don't forget to free() the returned buffer when you don't need it anymore. 654 | * 655 | * @returns fru_info_area_t *area A newly allocated buffer containing the created area 656 | * 657 | */ 658 | fru_board_area_t * fru_board_info(const fru_exploded_board_t *board) ///< [in] Exploded board information area 659 | { 660 | int i; 661 | 662 | if(!board) { 663 | errno = EFAULT; 664 | return NULL; 665 | } 666 | 667 | fru_reclist_t fields[] = { // List of fields. Mandatory fields are unallocated yet. 668 | [FRU_BOARD_MFG] = { NULL, &fields[FRU_BOARD_PRODNAME] }, 669 | [FRU_BOARD_PRODNAME] = { NULL, &fields[FRU_BOARD_SERIAL] }, 670 | [FRU_BOARD_SERIAL] = { NULL, &fields[FRU_BOARD_PARTNO] }, 671 | [FRU_BOARD_PARTNO] = { NULL, &fields[FRU_BOARD_FILE] }, 672 | [FRU_BOARD_FILE] = { NULL, board->cust }, 673 | }; 674 | 675 | const typed_field_t strings[] = { board->mfg, board->pname, board->serial, board->pn, board->file }; 676 | fru_board_area_t *out = NULL; 677 | 678 | out = (fru_board_area_t *)fru_create_info_area(FRU_BOARD_INFO, 679 | board->lang, &board->tv, fields, 680 | ARRAY_SZ(strings), strings); 681 | 682 | return out; 683 | } 684 | 685 | /** 686 | * Allocate and build a Product Information Area block. 687 | * 688 | * The function will allocate a buffer of size that is a muliple of 8 bytes 689 | * and is big enough to accomodate the standard area header, all the mandatory 690 | * fields, all the supplied custom fields, the required padding and a checksum byte. 691 | * 692 | * The mandatory fields will be encoded as fits best. 693 | * The custom fields will be used as is (pre-encoded). 694 | * 695 | * It is safe to free (deallocate) any arguments supplied to this function 696 | * immediately after the call as all the data is copied to the new buffer. 697 | * 698 | * Don't forget to free() the returned buffer when you don't need it anymore. 699 | * 700 | * @returns fru_info_area_t *area A newly allocated buffer containing the created area 701 | * 702 | */ 703 | fru_product_area_t * fru_product_info(const fru_exploded_product_t *product) ///< [in] Exploded product information area 704 | { 705 | int i; 706 | 707 | if(!product) { 708 | errno = EFAULT; 709 | return NULL; 710 | } 711 | 712 | fru_reclist_t fields[] = { // List of fields. Mandatory fields are unallocated yet. 713 | [FRU_PROD_MFG] = { NULL, &fields[FRU_PROD_NAME] }, 714 | [FRU_PROD_NAME] = { NULL, &fields[FRU_PROD_MODELPN] }, 715 | [FRU_PROD_MODELPN] = { NULL, &fields[FRU_PROD_VERSION] }, 716 | [FRU_PROD_VERSION] = { NULL, &fields[FRU_PROD_SERIAL] }, 717 | [FRU_PROD_SERIAL] = { NULL, &fields[FRU_PROD_ASSET] }, 718 | [FRU_PROD_ASSET] = { NULL, &fields[FRU_PROD_FILE] }, 719 | [FRU_PROD_FILE] = { NULL, product->cust }, 720 | }; 721 | 722 | const typed_field_t strings[] = { product->mfg, product->pname, 723 | product->pn, product->ver, 724 | product->serial, product->atag, 725 | product->file }; 726 | fru_product_area_t *out = NULL; 727 | 728 | out = fru_create_info_area(FRU_PRODUCT_INFO, 729 | product->lang, NULL, fields, 730 | ARRAY_SZ(strings), strings); 731 | 732 | return out; 733 | } 734 | 735 | /** 736 | * Take an input string, check that it looks like UUID, and pack it into 737 | * an "exploded" multirecord area record in binary form. 738 | * 739 | * @returns An errno-like negative error code 740 | * @retval 0 Success 741 | * @retval EINVAL Invalid UUID string (wrong length, wrong symbols) 742 | * @retval EFAULT Invalid pointer 743 | * @retval >0 any other error that calloc() is allowed to retrun 744 | */ 745 | int fru_mr_uuid2rec(fru_mr_rec_t **rec, const unsigned char *str) 746 | { 747 | size_t len; 748 | fru_mr_mgmt_rec_t *mgmt = NULL; 749 | 750 | const int UUID_SIZE = 16; 751 | const int UUID_STRLEN_NONDASHED = UUID_SIZE * 2; // 2 hex digits for byte 752 | const int UUID_STRLEN_DASHED = UUID_STRLEN_NONDASHED + 4; 753 | 754 | union __attribute__((packed)) { 755 | uint8_t raw[UUID_SIZE]; 756 | // The structure is according to DMTF SMBIOS 3.2 Specification 757 | struct __attribute__((packed)) { 758 | // All words and dwords here must be Little-Endian for SMBIOS 759 | uint32_t time_low; 760 | uint16_t time_mid; 761 | uint16_t time_hi_and_version; 762 | uint8_t clock_seq_hi_and_reserved; 763 | uint8_t clock_seq_low; 764 | uint8_t node[6]; 765 | }; 766 | } uuid; 767 | 768 | // Need a valid non-allocated record pointer and a string 769 | if (!rec || *rec) return -EFAULT; 770 | if (!str) return -EFAULT; 771 | 772 | len = strlen(str); 773 | if(UUID_STRLEN_DASHED != len && UUID_STRLEN_NONDASHED != len) { 774 | return -EINVAL; 775 | } 776 | 777 | mgmt = calloc(1, sizeof(fru_mr_mgmt_rec_t) + UUID_SIZE); 778 | if (!mgmt) return errno; 779 | 780 | mgmt->hdr.type_id = FRU_MR_MGMT_ACCESS; 781 | mgmt->hdr.eol_ver = FRU_MR_VER; 782 | mgmt->hdr.len = UUID_SIZE + 1; // Include the subtype byte 783 | mgmt->subtype = FRU_MR_MGMT_SYS_UUID; 784 | while(*str) { 785 | static size_t i = 0; 786 | int val; 787 | 788 | // Skip dashes 789 | if ('-' == *str) { 790 | ++str; 791 | continue; 792 | } 793 | 794 | if (!isxdigit(*str)) { 795 | free(mgmt); 796 | return -EINVAL; 797 | } 798 | 799 | val = toupper(*str); 800 | if (val < 'A') 801 | val = val - '0'; 802 | else 803 | val = val - 'A' + 0xA; 804 | 805 | if (0 == i % 2) 806 | uuid.raw[i / 2] = val << 4; 807 | else 808 | uuid.raw[i / 2] |= val; 809 | 810 | ++i; 811 | ++str; 812 | } 813 | 814 | // Ensure Little-Endian encoding for SMBIOS specification compatibility 815 | uuid.time_low = htole32(be32toh(uuid.time_low)); 816 | uuid.time_mid = htole16(be16toh(uuid.time_mid)); 817 | uuid.time_hi_and_version = htole16(be16toh(uuid.time_hi_and_version)); 818 | memcpy(mgmt->data, uuid.raw, UUID_SIZE); 819 | 820 | *rec = (fru_mr_rec_t *)mgmt; 821 | 822 | // Checksum the data 823 | mgmt->hdr.rec_checksum = calc_checksum((*rec)->data, mgmt->hdr.len); 824 | 825 | // Checksum the header, don't include the checksum byte itself 826 | mgmt->hdr.hdr_checksum = calc_checksum(*rec, sizeof(fru_mr_header_t) - 1); 827 | return 0; 828 | } 829 | 830 | /** 831 | * Allocate a new multirecord reclist entry and add it to \a reclist, 832 | * set \a reclist to point to the newly allocated entry if 833 | * \a reclist was NULL. 834 | * 835 | * @returns Pointer to the added entry 836 | */ 837 | fru_mr_reclist_t *add_mr_reclist(fru_mr_reclist_t **reclist) 838 | { 839 | fru_mr_reclist_t *rec; 840 | fru_mr_reclist_t *reclist_ptr = *reclist; 841 | rec = calloc(1, sizeof(*rec)); 842 | if(!rec) return NULL; 843 | 844 | // If the reclist is empty, update it 845 | if(!reclist_ptr) { 846 | *reclist = rec; 847 | } else { 848 | // If the reclist is not empty, find the last entry and append the new one as next 849 | while(reclist_ptr->next) 850 | reclist_ptr = reclist_ptr->next; 851 | 852 | reclist_ptr->next = rec; 853 | } 854 | 855 | return rec; 856 | } 857 | 858 | /** 859 | * Allocate and build a MultiRecord area block. 860 | * 861 | * The function will allocate a buffer of size that is required to store all 862 | * the provided data and accompanying record headers. It will calculate data 863 | * and header checksums automatically. 864 | * 865 | * All data will be copied as-is, without any additional encoding. 866 | * 867 | * It is safe to free (deallocate) any arguments supplied to this function 868 | * immediately after the call as all the data is copied to the new buffer. 869 | * 870 | * Don't forget to free() the returned buffer when you don't need it anymore. 871 | * 872 | * @returns fru_mr_area_t *area A newly allocated buffer containing the created area 873 | * 874 | */ 875 | fru_mr_area_t *fru_mr_area(fru_mr_reclist_t *reclist, size_t *total) 876 | { 877 | fru_mr_area_t *area = NULL; 878 | fru_mr_rec_t *rec; 879 | fru_mr_reclist_t *listitem = reclist; 880 | 881 | // Calculate the cumulative size of all records 882 | while (listitem && listitem->rec && listitem->rec->hdr.len) { 883 | *total += sizeof(fru_mr_header_t); 884 | *total += listitem->rec->hdr.len; 885 | listitem = listitem->next; 886 | } 887 | 888 | area = calloc(1, *total); 889 | if (!area) { 890 | *total = 0; 891 | return NULL; 892 | } 893 | 894 | // Walk the input records and pack them into an MR area 895 | listitem = reclist; 896 | rec = area; 897 | while (listitem && listitem->rec && listitem->rec->hdr.len) { 898 | size_t rec_sz = sizeof(fru_mr_header_t) + listitem->rec->hdr.len; 899 | memcpy(rec, listitem->rec, rec_sz); 900 | if (!listitem->next) { 901 | // Update the header and its checksum. Don't include the 902 | // checksum byte itself. 903 | size_t checksum_span = sizeof(fru_mr_header_t) - 1; 904 | rec->hdr.eol_ver |= FRU_MR_EOL; 905 | rec->hdr.hdr_checksum = calc_checksum(rec, checksum_span); 906 | } 907 | rec = (void *)rec + rec_sz; 908 | listitem = listitem->next; 909 | } 910 | 911 | return area; 912 | } 913 | 914 | /** 915 | * Create a FRU information file. 916 | * 917 | * @param[in] area The array of 5 areas, each may be NULL. 918 | * Areas must be given in the FRU order, which is: 919 | * internal use, chassis, board, product, multirecord 920 | * @param[out] size On success, the size of the newly created FRU information buffer, in 8-byte blocks 921 | * 922 | * @returns fru_t * buffer, a newly allocated buffer containing the created FRU information file 923 | */ 924 | fru_t * fru_create(fru_area_t area[FRU_MAX_AREAS], size_t *size) 925 | { 926 | fru_t fruhdr = { .ver = FRU_VER_1 }; 927 | int totalblocks = FRU_BLOCKS(sizeof(fru_t)); // Start with just the header size 928 | int area_offsets[FRU_MAX_AREAS] = { // Indices must match values of fru_area_type_t 929 | offsetof(fru_t, internal), 930 | offsetof(fru_t, chassis), 931 | offsetof(fru_t, board), 932 | offsetof(fru_t, product), 933 | offsetof(fru_t, multirec) 934 | }; 935 | fru_t *out = NULL; 936 | int i; 937 | 938 | // First calculate the total size of the FRU information storage file to be allocated. 939 | for(i = 0; i < FRU_MAX_AREAS; i++) { 940 | uint8_t atype = area[i].atype; 941 | uint8_t blocks = area[i].blocks; 942 | fru_info_area_t *data = area[i].data; 943 | 944 | // Area type must be valid and match the index 945 | if (!FRU_IS_ATYPE_VALID(atype) || atype != (uint8_t)FRU_AREA_NOT_PRESENT && atype != i) { 946 | errno = EINVAL; 947 | return NULL; 948 | } 949 | 950 | int area_offset_index = area_offsets[atype]; 951 | uint8_t *offset = (uint8_t *)&fruhdr + area_offset_index; 952 | 953 | if(!data || // No data is provided or 954 | !FRU_AREA_HAS_SIZE(atype) && !blocks || // no size is given for a non-sized area or 955 | !((fru_info_area_t *)data)->blocks // the sized area contains a zero size 956 | ) { 957 | // Mark the area as 958 | *offset = 0; 959 | continue; 960 | } 961 | 962 | if(!blocks) { 963 | blocks = data->blocks; 964 | area[i].blocks = blocks; 965 | } 966 | 967 | *offset = totalblocks; 968 | totalblocks += blocks; 969 | } 970 | 971 | // Calcute header checksum 972 | fruhdr.hchecksum = calc_checksum(&fruhdr, sizeof(fruhdr)); 973 | out = calloc(1, FRU_BYTES(totalblocks)); 974 | 975 | DEBUG("alocated a buffer at %p\n", out); 976 | if (!out) return NULL; 977 | 978 | memcpy(out, (uint8_t *)&fruhdr, sizeof(fruhdr)); 979 | 980 | // Now go through the areas again and copy them into the allocated buffer. 981 | // We have all the offsets and sizes set in the previous loop. 982 | for(i = 0; i < FRU_MAX_AREAS; i++) { 983 | uint8_t atype = area[i].atype; 984 | uint8_t blocks = area[i].blocks; 985 | uint8_t *data = area[i].data; 986 | int area_offset_index = area_offsets[atype]; 987 | uint8_t *offset = (uint8_t *)&fruhdr + area_offset_index; 988 | uint8_t *dst = (void *)out + FRU_BYTES(*offset); 989 | 990 | if (!blocks) continue; 991 | 992 | DEBUG("copying %d bytes of area of type %d to offset 0x%03X (0x%03lX)\n", 993 | FRU_BYTES(blocks), atype, FRU_BYTES(*offset), dst - (uint8_t *)out 994 | ); 995 | memcpy(dst, data, FRU_BYTES(blocks)); 996 | } 997 | 998 | *size = totalblocks; 999 | return out; 1000 | } 1001 | 1002 | 1003 | #ifdef __STANDALONE__ 1004 | 1005 | void dump(int len, const unsigned char *data) 1006 | { 1007 | int i; 1008 | printf("Data Dump:"); 1009 | for (i = 0; i < len; i++) { 1010 | if (!(i % 16)) printf("\n%04X: ", i); 1011 | printf("%02X ", data[i]); 1012 | } 1013 | printf("\n"); 1014 | } 1015 | 1016 | void test_encodings(void) 1017 | { 1018 | int i, len; 1019 | uint8_t typelen; 1020 | unsigned char *test_strings[] = { 1021 | /* 6-bit ASCII */ 1022 | "IPMI", "OK!", 1023 | /* BCD plus */ 1024 | "1234-56-7.89 01", 1025 | /* Simple text */ 1026 | "This is a simple text, with punctuation & other stuff", 1027 | /* Binary */ 1028 | "\x00\x01\x02\x03\x04\x05 BINARY TEST" 1029 | }; 1030 | unsigned char *test_types[] = { 1031 | "6-bit", "6-bit", 1032 | "BCPplus", 1033 | "Simple text", 1034 | "Binary" 1035 | }; 1036 | int test_lengths[] = { LEN_AUTO, LEN_AUTO, LEN_AUTO, LEN_AUTO, 18 }; 1037 | 1038 | for(i = 0; i < ARRAY_SZ(test_strings); i++) { 1039 | fru_field_t *field; 1040 | const unsigned char *out; 1041 | 1042 | printf("Data set %d.\n", i); 1043 | printf("Original data "); 1044 | if (test_lengths[i]) dump(test_lengths[i], test_strings[i]); 1045 | else printf(": [%s]\n", test_strings[i]); 1046 | 1047 | printf("Original type: %s\n", test_types[i]); 1048 | printf("Encoding... "); 1049 | field = fru_encode_data(test_lengths[i], test_strings[i]); 1050 | if (FRU_FIELD_TERMINATOR == field->typelen) { 1051 | printf("FAIL!\n\n"); 1052 | continue; 1053 | } 1054 | 1055 | printf("OK\n"); 1056 | printf("Encoded type is: "); 1057 | switch((field->typelen & __TYPE_BITS_MASK) >> __TYPE_BITS_SHIFT) { 1058 | case __TYPE_TEXT: 1059 | printf("Simple text\n"); 1060 | break; 1061 | case __TYPE_ASCII_6BIT: 1062 | printf("6-bit\n"); 1063 | break; 1064 | case __TYPE_BCDPLUS: 1065 | printf("BCDplus\n"); 1066 | break; 1067 | default: 1068 | printf("Binary\n"); 1069 | break; 1070 | } 1071 | 1072 | printf("Encoded data "); 1073 | dump(FRU_FIELDSIZE(field->typelen), (uint8_t *)field); 1074 | printf("Decoding... "); 1075 | 1076 | out = fru_decode_data(field); 1077 | if (!out) { 1078 | printf("FAIL!"); 1079 | goto next; 1080 | } 1081 | 1082 | printf("Decoded data "); 1083 | if (FRU_ISTYPE(field->typelen, BINARY)) { 1084 | dump(FRU_FIELDDATALEN(field->typelen), out); 1085 | } 1086 | else { 1087 | printf(": [%s]\n", out); 1088 | } 1089 | 1090 | printf("Comparing... "); 1091 | if (test_lengths[i] && !memcmp(test_strings[i], out, test_lengths[i]) || 1092 | !strcmp(test_strings[i], out)) 1093 | { 1094 | printf("OK!"); 1095 | } 1096 | else { 1097 | printf("FAIL!"); 1098 | } 1099 | 1100 | free((void *)out); 1101 | next: 1102 | free((void *)field); 1103 | printf("\n\n"); 1104 | } 1105 | } 1106 | 1107 | int main(int argc, char *argv[]) 1108 | { 1109 | test_encodings(); 1110 | exit(1); 1111 | } 1112 | #endif 1113 | -------------------------------------------------------------------------------- /fru.h: -------------------------------------------------------------------------------- 1 | /** @file 2 | * @brief Header for FRU information helper functions 3 | * 4 | * Copyright (C) 2016-2021 Alexander Amelkin 5 | * SPDX-License-Identifier: LGPL-2.0-or-later OR Apache-2.0 6 | */ 7 | 8 | #ifndef __FRULIB_FRU_H__ 9 | #define __FRULIB_FRU_H__ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define ARRAY_SZ(a) (sizeof(a) / sizeof((a)[0])) 18 | 19 | typedef struct fru_s { 20 | uint8_t ver:4, rsvd:4; 21 | uint8_t internal; 22 | uint8_t chassis; 23 | uint8_t board; 24 | uint8_t product; 25 | uint8_t multirec; 26 | uint8_t pad; 27 | uint8_t hchecksum; ///< Header checksum 28 | uint8_t data[]; 29 | } fru_t; 30 | 31 | typedef enum fru_area_type_e { 32 | FRU_AREA_NOT_PRESENT = -1, 33 | FRU_INTERNAL_USE, 34 | FRU_CHASSIS_INFO, 35 | FRU_BOARD_INFO, 36 | FRU_PRODUCT_INFO, 37 | FRU_MULTIRECORD, 38 | FRU_MAX_AREAS 39 | } fru_area_type_t; 40 | 41 | typedef enum { 42 | FRU_CHASSIS_PARTNO, 43 | FRU_CHASSIS_SERIAL 44 | } fru_chassis_field_t; 45 | 46 | typedef enum { 47 | FRU_BOARD_MFG, 48 | FRU_BOARD_PRODNAME, 49 | FRU_BOARD_SERIAL, 50 | FRU_BOARD_PARTNO, 51 | FRU_BOARD_FILE 52 | } fru_board_field_t; 53 | 54 | typedef enum { 55 | FRU_PROD_MFG, 56 | FRU_PROD_NAME, 57 | FRU_PROD_MODELPN, 58 | FRU_PROD_VERSION, 59 | FRU_PROD_SERIAL, 60 | FRU_PROD_ASSET, 61 | FRU_PROD_FILE 62 | } fru_prod_field_t; 63 | 64 | /// Table 16-2, MultiRecord Area Record Types 65 | typedef enum { 66 | FRU_MR_MIN = 0x00, 67 | FRU_MR_PSU_INFO = 0x00, 68 | FRU_MR_DC_OUT = 0x01, 69 | FRU_MR_DC_LOAD = 0x02, 70 | FRU_MR_MGMT_ACCESS = 0x03, 71 | FRU_MR_BASE_COMPAT = 0x04, 72 | FRU_MR_EXT_COMPAT = 0x05, 73 | FRU_MR_ASF_FIXED_SMBUS = 0x06, 74 | FRU_MR_ASF_LEGACY_ALERTS = 0x07, 75 | FRU_MR_ASF_REMOTE_CTRL = 0x08, 76 | FRU_MR_EXT_DC_OUT = 0x09, 77 | FRU_MR_EXT_DC_LOAD = 0x0A, 78 | FRU_MR_NVME_B = 0x0B, 79 | FRU_MR_NVME_C = 0x0C, 80 | FRU_MR_NVME_D = 0x0D, 81 | FRU_MR_NVME_E = 0x0E, 82 | FRU_MR_NVME_F = 0x0F, 83 | FRU_MR_OEM_START = 0xC0, 84 | FRU_MR_OEM_END = 0xFF, 85 | FRU_MR_MAX = 0xFF 86 | } fru_mr_type_t; 87 | 88 | /// Table 18-6, Management Access Record 89 | typedef enum { 90 | FRU_MR_MGTM_MIN = 0x01, 91 | FRU_MR_MGMT_SYS_URL = 0x01, 92 | FRU_MR_MGMT_SYS_NAME = 0x02, 93 | FRU_MR_MGMT_SYS_PING = 0x03, 94 | FRU_MR_MGMT_COMPONENT_URL = 0x04, 95 | FRU_MR_MGMT_COMPONENT_NAME = 0x05, 96 | FRU_MR_MGMT_COMPONENT_PING = 0x06, 97 | FRU_MR_MGMT_SYS_UUID = 0x07, 98 | FRU_MR_MGTM_MAX = 0x07, 99 | } fru_mr_mgmt_type_t; 100 | 101 | #define FRU_IS_ATYPE_VALID(t) ((t) >= FRU_AREA_NOT_PRESENT && (t) < FRU_MAX_AREAS) 102 | 103 | /** 104 | * FRU area description structure. 105 | * 106 | * Contains information about a single arbitrary area. 107 | */ 108 | typedef struct fru_area_s { 109 | fru_area_type_t atype; /**< FRU area type */ 110 | uint8_t blocks; /**< Size of the data field in 8-byte blocks */ 111 | void * data; /**< Pointer to the actual FRU area data */ 112 | } fru_area_t; 113 | 114 | /** 115 | * Generic FRU area field header structure. 116 | * 117 | * Every field in chassis, board and product information areas has such a header. 118 | */ 119 | typedef struct fru_field_s { 120 | uint8_t typelen; /**< Type/length of the field */ 121 | uint8_t data[]; /**< The field data */ 122 | } fru_field_t; 123 | 124 | 125 | /** 126 | * A single-linked list of FRU area fields. 127 | * 128 | * This is used to describe any length chains of fields. 129 | * Mandatory fields are linked first in the order they appear 130 | * in the information area (as per Specification), then custom 131 | * fields are linked. 132 | */ 133 | typedef struct fru_reclist_s { 134 | fru_field_t *rec; /**< A pointer to the field or NULL if not initialized */ 135 | struct fru_reclist_s *next; /**< The next record in the list or NULL if last */ 136 | } fru_reclist_t; 137 | 138 | /** 139 | * Allocate a new reclist entry and add it to \a reclist, 140 | * set \a reclist to point to the newly allocated entry if 141 | * \a reclist was NULL. 142 | * 143 | * @returns Pointer to the added entry 144 | */ 145 | static inline fru_reclist_t *add_reclist(fru_reclist_t **reclist) 146 | { 147 | fru_reclist_t *rec; 148 | fru_reclist_t *reclist_ptr = *reclist; 149 | rec = calloc(1, sizeof(*rec)); 150 | if(!rec) return NULL; 151 | 152 | // If the reclist is empty, update it 153 | if(!reclist_ptr) { 154 | *reclist = rec; 155 | } else { 156 | // If the reclist is not empty, find the last entry and append the new one as next 157 | while(reclist_ptr->next) 158 | reclist_ptr = reclist_ptr->next; 159 | 160 | reclist_ptr->next = rec; 161 | } 162 | 163 | return rec; 164 | } 165 | 166 | /// Works both for fru_reclist_t* and for fru_mr_reclist_t* 167 | #define free_reclist(recp) while(recp) { \ 168 | typeof(recp->next) next = recp->next; \ 169 | free(recp->rec); \ 170 | free(recp); \ 171 | recp = next; \ 172 | } 173 | 174 | #define FRU_VER_1 1 175 | #define FRU_MINIMUM_AREA_HEADER \ 176 | uint8_t ver; /**< Area format version, only lower 4 bits */ 177 | 178 | #define FRU_INFO_AREA_HEADER \ 179 | FRU_MINIMUM_AREA_HEADER; \ 180 | uint8_t blocks; /**< Size in 8-byte blocks */ \ 181 | uint8_t langtype /**< Area language code or chassis type (from smbios.h) */ 182 | 183 | #define LANG_DEFAULT 0 184 | #define LANG_ENGLISH 25 185 | 186 | typedef struct fru_info_area_s { // The generic info area structure 187 | FRU_INFO_AREA_HEADER; 188 | uint8_t data[]; 189 | } fru_info_area_t; 190 | 191 | #define FRU_INFO_AREA_HEADER_SZ sizeof(fru_info_area_t) 192 | 193 | typedef struct fru_internal_use_area_s { 194 | FRU_MINIMUM_AREA_HEADER; 195 | uint8_t data[]; 196 | } fru_internal_use_area_t; 197 | 198 | typedef fru_info_area_t fru_chassis_area_t; 199 | 200 | typedef struct fru_board_area_s { 201 | FRU_INFO_AREA_HEADER; 202 | uint8_t mfgdate[3]; ///< Manufacturing date/time in seconds since 1996/1/1 0:00 203 | uint8_t data[]; ///< Variable size (multiple of 8 bytes) data with tail padding and checksum 204 | } fru_board_area_t; 205 | 206 | typedef fru_info_area_t fru_product_area_t; 207 | 208 | typedef struct { 209 | uint8_t type_id; ///< Record Type ID 210 | uint8_t eol_ver; 211 | #define FRU_MR_EOL 0x80 // End-of-list flag 212 | #define FRU_MR_VER_MASK 0x07 213 | #define FRU_MR_VER 0x02 // Version is always 2 214 | uint8_t len; ///< Length of the raw `data` array 215 | uint8_t rec_checksum; 216 | uint8_t hdr_checksum; 217 | } __attribute__((packed)) fru_mr_header_t; 218 | 219 | typedef struct { 220 | fru_mr_header_t hdr; 221 | uint8_t data[]; ///< Raw data of size `len` 222 | } __attribute__((packed)) fru_mr_rec_t; 223 | 224 | typedef struct { 225 | fru_mr_header_t hdr; 226 | uint8_t subtype; 227 | uint8_t data[]; 228 | } __attribute__((packed)) fru_mr_mgmt_rec_t; 229 | 230 | typedef struct fru_mr_reclist_s { 231 | fru_mr_rec_t *rec; 232 | struct fru_mr_reclist_s *next; 233 | } fru_mr_reclist_t; 234 | 235 | typedef fru_mr_rec_t fru_mr_area_t; /// Intended for use as a pointer only 236 | 237 | #define FRU_AREA_HAS_DATE(t) (FRU_BOARD_INFO == (t)) 238 | #define FRU_DATE_AREA_HEADER_SZ sizeof(fru_board_area_t) 239 | #define FRU_DATE_UNSPECIFIED 0 240 | 241 | #define FRU_AREA_HAS_SIZE(t) (FRU_INTERNAL_USE < (t) && (t) < FRU_MULTIRECORD) 242 | #define FRU_AREA_HAS_HEADER(t) (FRU_MULTIRECORD != (t)) 243 | #define FRU_AREA_IS_GENERIC(t) FRU_AREA_HAS_SIZE(t) 244 | 245 | #define __TYPE_BITS_SHIFT 6 246 | #define __TYPE_BITS_MASK 0xC0 247 | #define __TYPE_BINARY 0x00 248 | #define __TYPE_BCDPLUS 0x01 249 | #define __TYPE_ASCII_6BIT 0x02 250 | #define __TYPE_TEXT 0x03 251 | 252 | /** FRU field type. Any of BINARY, BCDPLUS, ASCII_6BIT or TEXT. */ 253 | #define FRU_MAKETYPE(x) (__TYPE_##x << __TYPE_BITS_SHIFT) 254 | #define FRU_FIELDDATALEN(x) ((x) & ~__TYPE_BITS_MASK) 255 | #define FRU_FIELDMAXLEN FRU_FIELDDATALEN(UINT8_MAX) // For FRU fields 256 | #define FRU_FIELDMAXARRAY (FRU_FIELDMAXLEN + 1) // For C array allocation 257 | #define FRU_FIELDSIZE(typelen) (FRU_FIELDDATALEN(typelen) + sizeof(fru_field_t)) 258 | #define FRU_TYPELEN(t, l) (FRU_MAKETYPE(t) | FRU_FIELDDATALEN(l)) 259 | #define FRU_TYPE(t) (((t) & __TYPE_BITS_MASK) >> __TYPE_BITS_SHIFT) 260 | #define FRU_ISTYPE(t, type) (FRU_TYPE(t) == __TYPE_##type) 261 | 262 | #define LEN_AUTO 0 263 | #define LEN_BCDPLUS -1 264 | #define LEN_6BITASCII -2 265 | #define LEN_TEXT -3 266 | 267 | #define FRU_6BIT_LENGTH(len) (((len) * 3 + 3) / 4) 268 | #define FRU_6BIT_FULLLENGTH(l6) (((l6) * 4) / 3) 269 | #define FRU_FIELD_EMPTY FRU_TYPELEN(TEXT, 0) 270 | #define FRU_FIELD_TERMINATOR FRU_TYPELEN(TEXT, 1) 271 | 272 | #define FRU_BLOCK_SZ 8 273 | #define FRU_BYTES(blocks) ((blocks) * FRU_BLOCK_SZ) 274 | #define FRU_BLOCKS(bytes) (((bytes) + FRU_BLOCK_SZ - 1) / FRU_BLOCK_SZ) 275 | 276 | typedef struct { 277 | enum { 278 | FIELD_TYPE_AUTO, 279 | FIELD_TYPE_BINARY, 280 | FIELD_TYPE_BCDPLUS, 281 | FIELD_TYPE_SIXBITASCII, 282 | FIELD_TYPE_TEXT 283 | } type; 284 | unsigned char val[FRU_FIELDMAXARRAY]; 285 | } typed_field_t; 286 | 287 | typedef struct { 288 | uint8_t type; 289 | typed_field_t pn; 290 | typed_field_t serial; 291 | fru_reclist_t *cust; 292 | } fru_exploded_chassis_t; 293 | 294 | typedef struct { 295 | uint8_t lang; 296 | struct timeval tv; 297 | typed_field_t mfg; 298 | typed_field_t pname; 299 | typed_field_t serial; 300 | typed_field_t pn; 301 | typed_field_t file; 302 | fru_reclist_t *cust; 303 | } fru_exploded_board_t; 304 | 305 | typedef struct { 306 | uint8_t lang; 307 | typed_field_t mfg; 308 | typed_field_t pname; 309 | typed_field_t pn; 310 | typed_field_t ver; 311 | typed_field_t serial; 312 | typed_field_t atag; 313 | typed_field_t file; 314 | fru_reclist_t *cust; 315 | } fru_exploded_product_t; 316 | 317 | #define fru_loadfield(eafield, value) strncpy(eafield, value, FRU_FIELDMAXLEN) 318 | 319 | void fru_set_autodetect(bool enable); 320 | 321 | fru_chassis_area_t * fru_chassis_info(const fru_exploded_chassis_t *chassis); 322 | fru_board_area_t * fru_board_info(const fru_exploded_board_t *board); 323 | fru_product_area_t * fru_product_info(const fru_exploded_product_t *product); 324 | 325 | int fru_mr_uuid2rec(fru_mr_rec_t **rec, const unsigned char *str); 326 | fru_mr_reclist_t * add_mr_reclist(fru_mr_reclist_t **reclist); 327 | fru_mr_area_t * fru_mr_area(fru_mr_reclist_t *reclist, size_t *total); 328 | 329 | fru_field_t * fru_encode_data(int len, const uint8_t *data); 330 | fru_t * fru_create(fru_area_t area[FRU_MAX_AREAS], size_t *size); 331 | 332 | #endif // __FRULIB_FRU_H__ 333 | -------------------------------------------------------------------------------- /frugen.c: -------------------------------------------------------------------------------- 1 | /** @file 2 | * @brief FRU generator utility 3 | * 4 | * Copyright (C) 2016-2021 Alexander Amelkin 5 | * SPDX-License-Identifier: GPL-2.0-or-later OR Apache-2.0 6 | */ 7 | #ifndef VERSION 8 | #define VERSION "v1.2-dirty-orphan" 9 | #endif 10 | 11 | #define COPYRIGHT_YEARS "2016-2021" 12 | 13 | #define _GNU_SOURCE 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include "fru.h" 26 | #include "smbios.h" 27 | 28 | #ifdef __HAS_JSON__ 29 | #include 30 | #endif 31 | 32 | #define fatal(fmt, args...) do { \ 33 | fprintf(stderr, fmt, ##args); \ 34 | fprintf(stderr, "\n"); \ 35 | exit(1); \ 36 | } while(0) 37 | 38 | volatile int debug_level = 0; 39 | #define debug(level, fmt, args...) do { \ 40 | int e = errno; \ 41 | if(level <= debug_level) { \ 42 | printf("DEBUG: "); \ 43 | errno = e; \ 44 | printf(fmt, ##args); \ 45 | printf("\n"); \ 46 | errno = e; \ 47 | } \ 48 | } while(0) 49 | 50 | static 51 | void hexdump(const void *data, size_t len) 52 | { 53 | size_t i; 54 | const unsigned char *buf = data; 55 | 56 | for (i = 0; i < len; ++i) { 57 | if (0 == (i % 16)) 58 | printf("DEBUG: %04x: ", (unsigned int)i); 59 | 60 | printf("%02X ", buf[i]); 61 | 62 | if (15 == (i % 16)) 63 | printf("\n"); 64 | } 65 | 66 | if (i % 16) { 67 | printf("\n"); 68 | } 69 | } 70 | 71 | #define debug_dump(level, data, len, fmt, args...) do { \ 72 | debug(level, fmt, ##args); \ 73 | if (level <= debug_level) hexdump(data, len); \ 74 | } while(0) 75 | 76 | /** 77 | * Convert 2 bytes of hex string into a binary byte 78 | */ 79 | static 80 | long hex2byte(const char *hex) { 81 | static const long hextable[256] = { 82 | [0 ... 255] = -1, 83 | ['0'] = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 84 | ['A'] = 10, 11, 12, 13, 14, 15, 85 | ['a'] = 10, 11, 12, 13, 14, 15 86 | }; 87 | 88 | if (!hex) return -1; 89 | 90 | long hi = hextable[hex[0]]; 91 | long lo = hextable[hex[1]]; 92 | 93 | debug(9, "hi = %02lX, lo = %02lX", hi, lo); 94 | 95 | if (hi < 0 || lo < 0) 96 | return -1; 97 | 98 | return ((hi << 4) | lo); 99 | } 100 | 101 | static 102 | bool datestr_to_tv(const char *datestr, struct timeval *tv) 103 | { 104 | struct tm tm = {0}; 105 | time_t time; 106 | char *ret; 107 | 108 | #if __WIN32__ || __WIN64__ 109 | /* There is no strptime() in Windows C libraries */ 110 | int mday, mon, year, hour, min, sec; 111 | 112 | if(6 != sscanf(datestr, "%d/%d/%d %d:%d:%d", &mday, &mon, &year, &hour, &min, &sec)) { 113 | return false; 114 | } 115 | 116 | tm.tm_mday = mday; 117 | tm.tm_mon = mon - 1; 118 | tm.tm_year = year - 1900; 119 | tm.tm_hour = hour; 120 | tm.tm_min = min; 121 | tm.tm_sec = sec; 122 | #else 123 | ret = strptime(datestr, "%d/%m/%Y%t%T", &tm); 124 | if (!ret || *ret != 0) 125 | return false; 126 | #endif 127 | tzset(); // Set up local timezone 128 | tm.tm_isdst = -1; // Use local timezone data in mktime 129 | time = mktime(&tm); // Here we have local time since local Epoch 130 | tv->tv_sec = time + timezone; // Convert to UTC 131 | tv->tv_usec = 0; 132 | return true; 133 | } 134 | 135 | static 136 | uint8_t * fru_encode_binary_string(size_t *len, const char *hexstr) 137 | { 138 | int i; 139 | uint8_t *buf; 140 | 141 | if (!len) { 142 | fatal("BUG: No storage for hex string length provided"); 143 | } 144 | 145 | *len = strlen(hexstr); 146 | debug(3, "The field is marked as binary, length is %zi", *len); 147 | if (*len % 2) 148 | fatal("Must provide even number of nibbles for binary data"); 149 | *len /= 2; 150 | buf = malloc(*len); 151 | if (!buf) 152 | fatal("Failed to allocate a buffer for binary data"); 153 | for (i = 0; i < *len; i++) { 154 | long byte = hex2byte(hexstr + 2 * i); 155 | debug(4, "[%d] %c %c => 0x%02lX", 156 | i, hexstr[2 * i], hexstr[2 * i + 1], byte); 157 | if (byte < 0) 158 | fatal("Invalid hex data provided for binary attribute"); 159 | buf[i] = byte; 160 | } 161 | return buf; 162 | } 163 | 164 | static 165 | fru_field_t * fru_encode_custom_binary_field(const char *hexstr) 166 | { 167 | size_t len; 168 | uint8_t *buf = fru_encode_binary_string(&len, hexstr); 169 | fru_field_t *rec; 170 | rec = fru_encode_data(len, buf); 171 | free(buf); 172 | 173 | return rec; 174 | } 175 | 176 | #ifdef __HAS_JSON__ 177 | static 178 | bool json_fill_fru_area_fields(json_object *jso, int count, 179 | const char *fieldnames[], 180 | typed_field_t *fields[]) 181 | { 182 | int i; 183 | json_object *jsfield; 184 | bool data_in_this_area = false; 185 | for (i = 0; i < count; i++) { 186 | if (json_object_object_get_ex(jso, fieldnames[i], &jsfield)) { 187 | // field is an object 188 | json_object *typefield, *valfield; 189 | if (json_object_object_get_ex(jsfield, "type", &typefield) && 190 | json_object_object_get_ex(jsfield, "data", &valfield)) { 191 | // expected subfields are type and val 192 | const char *type = json_object_get_string(typefield); 193 | const char *val = json_object_get_string(valfield); 194 | if (!strcmp("binary", type)) { 195 | fatal(1, "Binary format not yet implemented"); 196 | } else if (!strcmp("bcdplus", type)) { 197 | fields[i]->type = FIELD_TYPE_BCDPLUS; 198 | } else if (!strcmp("6bitascii", type)) { 199 | fields[i]->type = FIELD_TYPE_SIXBITASCII; 200 | } else if (!strcmp("text", type)) { 201 | fields[i]->type = FIELD_TYPE_TEXT; 202 | } else { 203 | debug(1, "Unknown type %s for field '%s'", 204 | type, fieldnames[i]); 205 | continue; 206 | } 207 | fru_loadfield(fields[i]->val, val); 208 | debug(2, "Field %s '%s' (%s) loaded from JSON", 209 | fieldnames[i], val, type); 210 | data_in_this_area = true; 211 | } else { 212 | const char *s = json_object_get_string(jsfield); 213 | debug(2, "Field %s '%s' loaded from JSON", 214 | fieldnames[i], s); 215 | fru_loadfield(fields[i]->val, s); 216 | fields[i]->type = FIELD_TYPE_AUTO; 217 | data_in_this_area = true; 218 | } 219 | } 220 | } 221 | 222 | return data_in_this_area; 223 | } 224 | 225 | static 226 | bool json_fill_fru_area_custom(json_object *jso, fru_reclist_t **custom) 227 | { 228 | int i, alen; 229 | json_object *jsfield; 230 | bool data_in_this_area = false; 231 | fru_reclist_t *custptr; 232 | 233 | if (!custom) 234 | return false; 235 | 236 | json_object_object_get_ex(jso, "custom", &jsfield); 237 | if (!jsfield) 238 | return false; 239 | 240 | if (json_object_get_type(jsfield) != json_type_array) 241 | return false; 242 | 243 | alen = json_object_array_length(jsfield); 244 | if (!alen) 245 | return false; 246 | 247 | for (i = 0; i < alen; i++) { 248 | const char *type = NULL; 249 | const char *data = NULL; 250 | json_object *item, *ifield; 251 | 252 | item = json_object_array_get_idx(jsfield, i); 253 | if (!item) continue; 254 | 255 | json_object_object_get_ex(item, "type", &ifield); 256 | if (!ifield || !(type = json_object_get_string(ifield))) { 257 | debug(3, "Using automatic text encoding for custom field %d", i); 258 | type = "auto"; 259 | } 260 | 261 | json_object_object_get_ex(item, "data", &ifield); 262 | if (!ifield || !(data = json_object_get_string(ifield))) { 263 | debug(3, "Emtpy data or no data at all found for custom field %d", i); 264 | continue; 265 | } 266 | 267 | debug(4, "Custom pointer before addition was %p", *custom); 268 | custptr = add_reclist(custom); 269 | debug(4, "Custom pointer after addition is %p", *custom); 270 | 271 | if (!custptr) 272 | return false; 273 | 274 | if (!strcmp("binary", type)) { 275 | custptr->rec = fru_encode_custom_binary_field(data); 276 | } else if (!strcmp("bcdplus", type)) { 277 | custptr->rec = fru_encode_data(LEN_BCDPLUS, data); 278 | } else if (!strcmp("6bitascii", type)) { 279 | custptr->rec = fru_encode_data(LEN_6BITASCII, data); 280 | } else if (!strcmp("text", type)) { 281 | custptr->rec = fru_encode_data(LEN_TEXT, data); 282 | } else { 283 | custptr->rec = fru_encode_data(LEN_AUTO, data); 284 | } 285 | 286 | if (!custptr->rec) { 287 | fatal("Failed to encode custom field. Memory allocation or field length problem."); 288 | } 289 | debug(2, "Custom field %i has been loaded from JSON at %p->rec = %p", i, *custom, (*custom)->rec); 290 | data_in_this_area = true; 291 | } 292 | 293 | debug(4, "Traversing all custom fields..."); 294 | custptr = *custom; 295 | while(custptr) { 296 | debug(4, "Custom %p, next %p", custptr, custptr->next); 297 | custptr = custptr->next; 298 | } 299 | 300 | return data_in_this_area; 301 | } 302 | 303 | static 304 | bool json_fill_fru_mr_reclist(json_object *jso, fru_mr_reclist_t **mr_reclist) 305 | { 306 | int i, alen; 307 | fru_mr_reclist_t *mr_reclist_tail; 308 | bool has_multirec = false; 309 | 310 | if (!mr_reclist) 311 | goto out; 312 | 313 | if (json_object_get_type(jso) != json_type_array) 314 | goto out; 315 | 316 | alen = json_object_array_length(jso); 317 | if (!alen) 318 | goto out; 319 | 320 | debug(4, "Multirecord area record list is initially at %p", *mr_reclist); 321 | mr_reclist_tail = add_mr_reclist(mr_reclist); 322 | if (!mr_reclist_tail) 323 | fatal("JSON: Failed to allocate multirecord area list"); 324 | 325 | debug(4, "Multirecord area record list is now at %p", *mr_reclist); 326 | debug(4, "Multirecord area record list tail is at %p", mr_reclist_tail); 327 | 328 | for (i = 0; i < alen; i++) { 329 | const char *type = NULL; 330 | json_object *item, *ifield; 331 | 332 | item = json_object_array_get_idx(jso, i); 333 | if (!item) continue; 334 | 335 | debug(3, "Parsing record #%d/%d", i + 1, alen); 336 | 337 | json_object_object_get_ex(item, "type", &ifield); 338 | if (!ifield || !(type = json_object_get_string(ifield))) { 339 | fatal("Each multirecord area record must have a type specifier"); 340 | } 341 | 342 | debug(3, "Record is of type '%s'", type); 343 | 344 | if (!strcmp(type, "management")) { 345 | const char *subtype = NULL; 346 | json_object_object_get_ex(item, "subtype", &ifield); 347 | if (!ifield || !(subtype = json_object_get_string(ifield))) { 348 | fatal("Each management record must have a subtype"); 349 | } 350 | 351 | debug(3, "Management record subtype is '%s'", subtype); 352 | 353 | if (!strcmp(subtype, "uuid")) { 354 | const unsigned char *uuid = NULL; 355 | json_object_object_get_ex(item, "uuid", &ifield); 356 | if (!ifield || !(uuid = json_object_get_string(ifield))) { 357 | fatal("A uuid management record must have a uuid field"); 358 | } 359 | 360 | debug(3, "Parsing UUID %s", uuid); 361 | errno = fru_mr_uuid2rec(&mr_reclist_tail->rec, uuid); 362 | if (errno) 363 | fatal("Failed to convert UUID: %m"); 364 | debug(2, "System UUID loaded from JSON: %s", uuid); 365 | has_multirec = true; 366 | } 367 | else { 368 | fatal("Management Access Record type '%s' " 369 | "is not supported", subtype); 370 | } 371 | } 372 | else { 373 | fatal("Multirecord type '%s' is not supported", type); 374 | continue; 375 | } 376 | } 377 | 378 | out: 379 | return has_multirec; 380 | } 381 | 382 | 383 | #endif /* __HAS_JSON__ */ 384 | 385 | int main(int argc, char *argv[]) 386 | { 387 | int i; 388 | int fd; 389 | int opt; 390 | int lindex; 391 | 392 | fru_t *fru; 393 | size_t size; 394 | 395 | bool cust_binary = false; // Flag: treat the following custom attribute as binary 396 | bool no_curr_date = false; // Flag: don't use current timestamp if no 'date' is specified 397 | 398 | const char *fname = NULL; 399 | 400 | fru_area_t areas[FRU_MAX_AREAS] = { 401 | { .atype = FRU_INTERNAL_USE }, 402 | { .atype = FRU_CHASSIS_INFO }, 403 | { .atype = FRU_BOARD_INFO, }, 404 | { .atype = FRU_PRODUCT_INFO, }, 405 | { .atype = FRU_MULTIRECORD } 406 | }; 407 | 408 | fru_exploded_chassis_t chassis = { 0, .type = SMBIOS_CHASSIS_UNKNOWN }; 409 | fru_exploded_board_t board = { 0, .lang = LANG_ENGLISH }; 410 | fru_exploded_product_t product = { 0, .lang = LANG_ENGLISH }; 411 | 412 | tzset(); 413 | gettimeofday(&board.tv, NULL); 414 | board.tv.tv_sec += timezone; 415 | 416 | struct option options[] = { 417 | /* Display usage help */ 418 | { .name = "help", .val = 'h', .has_arg = false }, 419 | 420 | /* Increase verbosity */ 421 | { .name = "verbose", .val = 'v', .has_arg = false }, 422 | 423 | /* Mark the following '*-custom' data as binary */ 424 | { .name = "binary", .val = 'b', .has_arg = false }, 425 | 426 | /* Disable autodetection, force ASCII encoding on standard fields, 427 | * Detection of binary (out of ASCII range) stays in place. 428 | */ 429 | { .name = "ascii", .val = 'I', .has_arg = false }, 430 | 431 | /* Set input file format to JSON */ 432 | { .name = "json", .val = 'j', .has_arg = false }, 433 | 434 | /* Set file to load the data from */ 435 | { .name = "from", .val = 'z', .has_arg = true }, 436 | 437 | /* Chassis info area related options */ 438 | { .name = "chassis-type", .val = 't', .has_arg = true }, 439 | { .name = "chassis-pn", .val = 'a', .has_arg = true }, 440 | { .name = "chassis-serial",.val = 'c', .has_arg = true }, 441 | { .name = "chassis-custom",.val = 'C', .has_arg = true }, 442 | /* Board info area related options */ 443 | { .name = "board-pname", .val = 'n', .has_arg = true }, 444 | { .name = "board-mfg", .val = 'm', .has_arg = true }, 445 | { .name = "board-date", .val = 'd', .has_arg = true }, 446 | { .name = "board-date-unspec", .val = 'u', .has_arg = false }, 447 | { .name = "board-pn", .val = 'p', .has_arg = true }, 448 | { .name = "board-serial", .val = 's', .has_arg = true }, 449 | { .name = "board-file", .val = 'f', .has_arg = true }, 450 | { .name = "board-custom", .val = 'B', .has_arg = true }, 451 | /* Product info area related options */ 452 | { .name = "prod-name", .val = 'N', .has_arg = true }, 453 | { .name = "prod-mfg", .val = 'G', .has_arg = true }, 454 | { .name = "prod-modelpn", .val = 'M', .has_arg = true }, 455 | { .name = "prod-version", .val = 'V', .has_arg = true }, 456 | { .name = "prod-serial", .val = 'S', .has_arg = true }, 457 | { .name = "prod-file", .val = 'F', .has_arg = true }, 458 | { .name = "prod-atag", .val = 'A', .has_arg = true }, 459 | { .name = "prod-custom", .val = 'P', .has_arg = true }, 460 | /* MultiRecord area related options */ 461 | { .name = "mr-uuid", .val = 'U', .has_arg = true }, 462 | }; 463 | 464 | const char *option_help[] = { 465 | ['h'] = "Display this help", 466 | ['v'] = "Increase program verbosity (debug) level", 467 | ['b'] = "Mark the next --*-custom option's argument as binary.\n\t\t" 468 | "Use hex string representation for the next custom argument.\n" 469 | "\n\t\t" 470 | "Example: frugen --binary --board-custom 0012DEADBEAF\n" 471 | "\n\t\t" 472 | "There must be an even number of characters in a 'binary' argument", 473 | ['I'] = "Disable auto-encoding on all fields, force ASCII.\n\t\t" 474 | "Out of ASCII range data will still result in binary encoding", 475 | ['j'] = "Set input text file format to JSON (default). Specify before '--from'", 476 | ['z'] = "Load FRU information from a text file", 477 | /* Chassis info area related options */ 478 | ['t'] = "Set chassis type (hex). Defaults to 0x02 ('Unknown')", 479 | ['a'] = "Set chassis part number", 480 | ['c'] = "Set chassis serial number", 481 | ['C'] = "Add a custom chassis information field, may be used multiple times", 482 | /* Board info area related options */ 483 | ['n'] = "Set board product name", 484 | ['m'] = "Set board manufacturer name", 485 | ['d'] = "Set board manufacturing date/time, use \"DD/MM/YYYY HH:MM:SS\" format.\n\t\t" 486 | "By default the current system date/time is used unless -u is specified", 487 | ['u'] = "Don't use current system date/time for board mfg. date, use 'Unspecified'", 488 | ['p'] = "Set board part number", 489 | ['s'] = "Set board serial number", 490 | ['f'] = "Set board FRU file ID", 491 | ['B'] = "Add a custom board information field, may be used multiple times", 492 | /* Product info area related options */ 493 | ['N'] = "Set product name", 494 | ['G'] = "Set product manufacturer name", 495 | ['M'] = "Set product model / part number", 496 | ['V'] = "Set product version", 497 | ['S'] = "Set product serial number", 498 | ['F'] = "Set product FRU file ID", 499 | ['A'] = "Set product Asset Tag", 500 | ['P'] = "Add a custom product information field, may be used multiple times", 501 | /* MultiRecord area related options */ 502 | ['U'] = "Set System Unique ID (UUID/GUID)", 503 | }; 504 | 505 | bool has_chassis = false, 506 | has_board = false, 507 | has_bdate = false, 508 | has_product = false, 509 | has_internal = false, 510 | has_multirec = false; 511 | fru_mr_reclist_t *mr_reclist = NULL; 512 | 513 | bool use_json = true; /* TODO: Add more input formats, consider libconfig */ 514 | 515 | unsigned char optstring[ARRAY_SZ(options) * 2 + 1] = {0}; 516 | 517 | for (i = 0; i < ARRAY_SZ(options); ++i) { 518 | static int k = 0; 519 | optstring[k++] = options[i].val; 520 | if (options[i].has_arg) 521 | optstring[k++] = ':'; 522 | } 523 | 524 | do { 525 | fru_reclist_t **custom = NULL; 526 | bool is_mr_record = false; // The current option is an MR area record 527 | lindex = -1; 528 | opt = getopt_long(argc, argv, optstring, options, &lindex); 529 | switch (opt) { 530 | case 'b': // binary 531 | debug(2, "Next custom field will be considered binary"); 532 | cust_binary = true; 533 | break; 534 | case 'I': // ASCII 535 | fru_set_autodetect(false); 536 | break; 537 | case 'v': // verbose 538 | debug_level++; 539 | debug(debug_level, "Verbosity level set to %d", debug_level); 540 | break; 541 | case 'h': // help 542 | printf("FRU Generator %s (c) %s, " 543 | "Alexander Amelkin \n", 544 | VERSION, COPYRIGHT_YEARS); 545 | printf("\n" 546 | "Usage: frugen [options] \n" 547 | "\n" 548 | "Options:\n\n"); 549 | for (i = 0; i < ARRAY_SZ(options); i++) { 550 | printf("\t-%c, --%s%s\n" /* "\t-%c%s\n" */, 551 | options[i].val, 552 | options[i].name, 553 | options[i].has_arg ? " " : ""); 554 | printf("\t\t%s.\n\n", option_help[options[i].val]); 555 | } 556 | printf("Example:\n" 557 | "\tfrugen --board-mfg \"Biggest International Corp.\" \\\n" 558 | "\t --board-pname \"Some Cool Product\" \\\n" 559 | "\t --board-pn \"BRD-PN-123\" \\\n" 560 | "\t --board-date \"10/1/2017 12:58:00\" \\\n" 561 | "\t --board-serial \"01171234\" \\\n" 562 | "\t --board-file \"Command Line\" \\\n" 563 | "\t --binary --board-custom \"01020304FEAD1E\" \\\n" 564 | "\t fru.bin\n"); 565 | exit(0); 566 | break; 567 | 568 | case 'j': // json 569 | use_json = true; 570 | break; 571 | 572 | case 'z': // from 573 | debug(2, "Will load FRU information from file %s", optarg); 574 | if (use_json) { 575 | #ifdef __HAS_JSON__ 576 | json_object *jstree, *jso, *jsfield; 577 | json_object_iter iter; 578 | 579 | debug(2, "Data format is JSON"); 580 | /* Allocate a new object and load contents from file */ 581 | jstree = json_object_from_file(optarg); 582 | if (NULL == jstree) 583 | fatal("Failed to load JSON FRU object from %s", optarg); 584 | 585 | json_object_object_foreachC(jstree, iter) { 586 | jso = iter.val; 587 | if (!strcmp(iter.key, "internal")) { 588 | fru_internal_use_area_t *internal; 589 | const char *data = json_object_get_string(jso); 590 | if (!data) { 591 | debug(2, "Internal use are w/o data, skipping"); 592 | continue; 593 | } 594 | fru_field_t *field; 595 | size_t datalen; 596 | char *encoded_data = 597 | fru_encode_binary_string(&datalen, data); 598 | size_t blocklen = FRU_BLOCKS(datalen + sizeof(*internal)); 599 | internal = calloc(1, FRU_BYTES(blocklen)); 600 | if (!internal) { 601 | fatal("Failed to allocate memory for internal use area"); 602 | } 603 | internal->ver = FRU_VER_1; 604 | memcpy(internal->data, encoded_data, datalen); 605 | free(encoded_data); 606 | areas[FRU_INTERNAL_USE].blocks = blocklen; 607 | areas[FRU_INTERNAL_USE].data = internal; 608 | 609 | debug(2, "Internal use area data loaded from JSON"); 610 | 611 | has_internal = true; 612 | continue; 613 | } else if (!strcmp(iter.key, "chassis")) { 614 | const char *fieldname[] = { "pn", "serial" }; 615 | typed_field_t* field[] = { &chassis.pn, &chassis.serial }; 616 | json_object_object_get_ex(jso, "type", &jsfield); 617 | if (jsfield) { 618 | chassis.type = json_object_get_int(jsfield); 619 | debug(2, "Chassis type 0x%02X loaded from JSON", chassis.type); 620 | has_chassis = true; 621 | } 622 | /* Now get values for the string fields */ 623 | has_chassis |= json_fill_fru_area_fields(jso, ARRAY_SZ(field), fieldname, field); 624 | has_chassis |= json_fill_fru_area_custom(jso, &chassis.cust); 625 | } else if (!strcmp(iter.key, "board")) { 626 | const char *fieldname[] = { "mfg", "pname", "pn", "serial", "file" }; 627 | typed_field_t *field[] = { &board.mfg, &board.pname, &board.pn, &board.serial, &board.file }; 628 | /* Get values for non-string fields */ 629 | #if 0 /* TODO: Language support is not implemented yet */ 630 | json_object_object_get_ex(jso, "lang", &jsfield); 631 | if (jsfield) { 632 | board.lang = json_object_get_int(jsfield); 633 | debug(2, "Board language 0x%02X loaded from JSON", board.lang); 634 | has_board = true; 635 | } 636 | #endif 637 | json_object_object_get_ex(jso, "date", &jsfield); 638 | if (jsfield) { 639 | const char *date = json_object_get_string(jsfield); 640 | debug(2, "Board date '%s' will be loaded from JSON", date); 641 | if (!datestr_to_tv(date, &board.tv)) 642 | fatal("Invalid board date/time format in JSON file"); 643 | has_board = true; 644 | has_bdate = true; 645 | } 646 | /* Now get values for the string fields */ 647 | has_board |= json_fill_fru_area_fields(jso, ARRAY_SZ(field), fieldname, field); 648 | has_board |= json_fill_fru_area_custom(jso, &board.cust); 649 | } else if (!strcmp(iter.key, "product")) { 650 | const char *fieldname[] = { "mfg", "pname", "pn", "ver", "serial", "atag", "file" }; 651 | typed_field_t *field[] = { &product.mfg, &product.pname, &product.pn, &product.ver, 652 | &product.serial, &product.atag, &product.file }; 653 | #if 0 /* TODO: Language support is not implemented yet */ 654 | /* Get values for non-string fields */ 655 | json_object_object_get_ex(jso, "lang", &jsfield); 656 | if (jsfield) { 657 | product.lang = json_object_get_int(jsfield); 658 | debug(2, "Product language 0x%02X loaded from JSON", product.lang); 659 | has_product = true; 660 | } 661 | #endif 662 | /* Now get values for the string fields */ 663 | has_product |= json_fill_fru_area_fields(jso, ARRAY_SZ(field), fieldname, field); 664 | has_product |= json_fill_fru_area_custom(jso, &product.cust); 665 | } else if (!strcmp(iter.key, "multirecord")) { 666 | debug(2, "Processing multirecord area records"); 667 | has_multirec |= json_fill_fru_mr_reclist(jso, &mr_reclist); 668 | } 669 | } 670 | 671 | /* Deallocate the JSON object */ 672 | json_object_put(jstree); 673 | #else 674 | fatal("JSON support was disabled at compile time"); 675 | #endif 676 | } 677 | else { 678 | fatal("The requested input file format is not supported"); 679 | } 680 | break; 681 | 682 | case 't': // chassis-type 683 | chassis.type = strtol(optarg, NULL, 16); 684 | debug(2, "Chassis type will be set to 0x%02X from [%s]", chassis.type, optarg); 685 | has_chassis = true; 686 | break; 687 | case 'a': // chassis-pn 688 | fru_loadfield(chassis.pn.val, optarg); 689 | has_chassis = true; 690 | break; 691 | case 'c': // chassis-serial 692 | fru_loadfield(chassis.serial.val, optarg); 693 | has_chassis = true; 694 | break; 695 | case 'C': // chassis-custom 696 | debug(2, "Custom chassis field [%s]", optarg); 697 | has_chassis = true; 698 | custom = &chassis.cust; 699 | break; 700 | case 'n': // board-pname 701 | fru_loadfield(board.pname.val, optarg); 702 | has_board = true; 703 | break; 704 | case 'm': // board-mfg 705 | fru_loadfield(board.mfg.val, optarg); 706 | has_board = true; 707 | break; 708 | case 'd': // board-date 709 | debug(2, "Board manufacturing date will be set from [%s]", optarg); 710 | if (!datestr_to_tv(optarg, &board.tv)) 711 | fatal("Invalid date/time format, use \"DD/MM/YYYY HH:MM:SS\""); 712 | has_board = true; 713 | break; 714 | case 'u': // board-date-unspec 715 | no_curr_date = true; 716 | break; 717 | case 'p': // board-pn 718 | fru_loadfield(board.pn.val, optarg); 719 | has_board = true; 720 | break; 721 | case 's': // board-sn 722 | fru_loadfield(board.serial.val, optarg); 723 | has_board = true; 724 | break; 725 | case 'f': // board-file 726 | fru_loadfield(board.file.val, optarg); 727 | has_board = true; 728 | break; 729 | case 'B': // board-custom 730 | debug(2, "Custom board field [%s]", optarg); 731 | has_board = true; 732 | custom = &board.cust; 733 | break; 734 | case 'N': // prod-name 735 | fru_loadfield(product.pname.val, optarg); 736 | has_product = true; 737 | break; 738 | case 'G': // prod-mfg 739 | fru_loadfield(product.mfg.val, optarg); 740 | has_product = true; 741 | break; 742 | case 'M': // prod-modelpn 743 | fru_loadfield(product.pn.val, optarg); 744 | has_product = true; 745 | break; 746 | case 'V': // prod-version 747 | fru_loadfield(product.ver.val, optarg); 748 | has_product = true; 749 | break; 750 | case 'S': // prod-serial 751 | fru_loadfield(product.serial.val, optarg); 752 | has_product = true; 753 | break; 754 | case 'F': // prod-file 755 | fru_loadfield(product.file.val, optarg); 756 | has_product = true; 757 | break; 758 | case 'A': // prod-atag 759 | fru_loadfield(product.atag.val, optarg); 760 | has_product = true; 761 | break; 762 | case 'P': // prod-custom 763 | debug(2, "Custom product field [%s]", optarg); 764 | has_product = true; 765 | custom = &product.cust; 766 | break; 767 | case 'U': // All multi-record options must be listed here 768 | // and processed later in a separate switch 769 | is_mr_record = true; 770 | break; 771 | 772 | case '?': 773 | exit(1); 774 | default: 775 | break; 776 | } 777 | 778 | if (is_mr_record) { 779 | fru_mr_reclist_t *mr_reclist_tail = add_mr_reclist(&mr_reclist); 780 | if (!mr_reclist_tail) { 781 | fatal("Failed to allocate multirecord area list"); 782 | } 783 | has_multirec = true; 784 | 785 | switch(opt) { 786 | case 'U': // UUID 787 | errno = fru_mr_uuid2rec(&mr_reclist_tail->rec, optarg); 788 | if (errno) { 789 | fatal("Failed to convert UUID: %m"); 790 | } 791 | break; 792 | default: 793 | fatal("Unknown multirecord option: %c", opt); 794 | } 795 | } 796 | 797 | if (custom) { 798 | fru_reclist_t *custptr; 799 | debug(3, "Adding a custom field from argument [%s]", optarg); 800 | custptr = add_reclist(custom); 801 | 802 | if (!custptr) 803 | fatal("Failed to allocate a custom record list entry"); 804 | 805 | if (cust_binary) { 806 | custptr->rec = fru_encode_custom_binary_field(optarg); 807 | } 808 | else { 809 | debug(3, "The custom field will be auto-typed"); 810 | custptr->rec = fru_encode_data(LEN_AUTO, optarg); 811 | } 812 | if (!custptr->rec) { 813 | fatal("Failed to encode custom field. Memory allocation or field length problem."); 814 | } 815 | cust_binary = false; 816 | } 817 | } while (opt != -1); 818 | 819 | if (optind >= argc) 820 | fatal("Filename must be specified"); 821 | 822 | fname = argv[optind]; 823 | debug(1, "FRU info data will be stored in %s", fname); 824 | 825 | if (has_internal) { 826 | debug(1, "FRU file will have an internal use area"); 827 | /* Nothing to do here, added for uniformity, the actual 828 | * internal use area can only be initialized from file */ 829 | } 830 | 831 | if (has_chassis) { 832 | int e; 833 | fru_chassis_area_t *ci = NULL; 834 | debug(1, "FRU file will have a chassis information area"); 835 | debug(3, "Chassis information area's custom field list is %p", chassis.cust); 836 | ci = fru_chassis_info(&chassis); 837 | e = errno; 838 | free_reclist(chassis.cust); 839 | 840 | if (ci) 841 | areas[FRU_CHASSIS_INFO].data = ci; 842 | else { 843 | errno = e; 844 | fatal("Error allocating a chassis info area: %m"); 845 | } 846 | } 847 | 848 | if (has_board) { 849 | int e; 850 | fru_board_area_t *bi = NULL; 851 | debug(1, "FRU file will have a board information area"); 852 | debug(3, "Board information area's custom field list is %p", board.cust); 853 | debug(3, "Board date is specified? = %d", has_bdate); 854 | debug(3, "Board date use unspec? = %d", no_curr_date); 855 | if (!has_bdate && no_curr_date) { 856 | debug(1, "Using 'unspecified' board mfg. date"); 857 | board.tv = (struct timeval){0}; 858 | } 859 | 860 | bi = fru_board_info(&board); 861 | e = errno; 862 | free_reclist(board.cust); 863 | 864 | if (bi) 865 | areas[FRU_BOARD_INFO].data = bi; 866 | else { 867 | errno = e; 868 | fatal("Error allocating a board info area: %m"); 869 | } 870 | } 871 | 872 | if (has_product) { 873 | int e; 874 | fru_product_area_t *pi = NULL; 875 | debug(1, "FRU file will have a product information area"); 876 | debug(3, "Product information area's custom field list is %p", product.cust); 877 | pi = fru_product_info(&product); 878 | 879 | e = errno; 880 | free_reclist(product.cust); 881 | 882 | if (pi) 883 | areas[FRU_PRODUCT_INFO].data = pi; 884 | else { 885 | errno = e; 886 | fatal("Error allocating a product info area: %m"); 887 | } 888 | } 889 | 890 | if (has_multirec) { 891 | int e; 892 | fru_mr_area_t *mr = NULL; 893 | size_t totalbytes = 0; 894 | debug(1, "FRU file will have a multirecord area"); 895 | debug(3, "Multirecord area record list is %p", mr_reclist); 896 | mr = fru_mr_area(mr_reclist, &totalbytes); 897 | 898 | e = errno; 899 | free_reclist(mr_reclist); 900 | 901 | if (mr) { 902 | areas[FRU_MULTIRECORD].data = mr; 903 | areas[FRU_MULTIRECORD].blocks = FRU_BLOCKS(totalbytes); 904 | 905 | debug_dump(3, mr, totalbytes, "Multirecord data:"); 906 | } 907 | else { 908 | errno = e; 909 | fatal("Error allocating a multirecord area: %m"); 910 | } 911 | } 912 | 913 | fru = fru_create(areas, &size); 914 | if (!fru) { 915 | fatal("Error allocating a FRU file buffer: %m"); 916 | } 917 | 918 | debug(1, "Writing %lu bytes of FRU data", (long unsigned int)FRU_BYTES(size)); 919 | 920 | fd = open(fname, 921 | #if __WIN32__ || __WIN64__ 922 | O_CREAT | O_TRUNC | O_WRONLY | O_BINARY, 923 | #else 924 | O_CREAT | O_TRUNC | O_WRONLY, 925 | #endif 926 | 0644); 927 | 928 | if (fd < 0) 929 | fatal("Couldn't create file %s: %m", fname); 930 | 931 | if (0 > write(fd, fru, FRU_BYTES(size))) 932 | fatal("Couldn't write to %s: %m", fname); 933 | 934 | free(fru); 935 | } 936 | -------------------------------------------------------------------------------- /smbios.h: -------------------------------------------------------------------------------- 1 | #ifndef __FRULIB_SMBIOS_CHASSIS_H__ 2 | #define __FRULIB_SMBIOS_CHASSIS_H__ 3 | 4 | /* 5 | * This file contains definitions for various enums defined 6 | * in DMTF SMBIOS Specification. 7 | */ 8 | 9 | enum { 10 | SMBIOS_CHASSIS_UNDEFINED, 11 | SMBIOS_CHASSIS_OTHER, 12 | SMBIOS_CHASSIS_UNKNOWN, 13 | SMBIOS_CHASSIS_DESKTOP, 14 | SMBIOS_CHASSIS_LPDESKTOP, 15 | SMBIOS_CHASSIS_PIZZABOX, 16 | SMBIOS_CHASSIS_MINITOWER, 17 | SMBIOS_CHASSIS_TOWER, 18 | SMBIOS_CHASSIS_PORTABLE, 19 | SMBIOS_CHASSIS_LAPTOP, 20 | SMBIOS_CHASSIS_NOTEBOOK, 21 | SMBIOS_CHASSIS_HANDHELD, 22 | SMBIOS_CHASSIS_DOCKSTATION, 23 | SMBIOS_CHASSIS_AIO, 24 | SMBIOS_CHASSIS_SUBNOTEBOOK, 25 | SMBIOS_CHASSIS_SPACESAVING, 26 | SMBIOS_CHASSIS_LUNCHBOX, 27 | SMBIOS_CHASSIS_MAINSERVER, 28 | SMBIOS_CHASSIS_EXPANSION, 29 | SMBIOS_CHASSIS_SUBCHASSIS, 30 | SMBIOS_CHASSIS_BUSEXPANSION, 31 | SMBIOS_CHASSIS_PERIPHERAL, 32 | SMBIOS_CHASSIS_RAID, 33 | SMBIOS_CHASSIS_RACKMOUNT, 34 | SMBIOS_CHASSIS_SEALED, 35 | SMBIOS_CHASSIS_MULTISYSTEM, 36 | SMBIOS_CHASSIS_COMPACT_PCI, 37 | SMBIOS_CHASSIS_ADVANCED_TCA, 38 | SMBIOS_CHASSIS_BLADE, 39 | SMBIOS_CHASSIS_BLADE_ENCLOSURE, 40 | SMBIOS_CHASSIS_TABLET, 41 | SMBIOS_CHASSIS_CONVERTIBLE, 42 | SMBIOS_CHASSIS_DETACHABLE, 43 | SMBIOS_CHASSIS_TYPES_TOTAL, 44 | }; 45 | 46 | #define SMBIOS_CHASSIS_IS_VALID(t) ((t) > SMBIOS_CHASSIS_UNDEFINED && (t) < SMBIOS_CHASSIS_TYPES_TOTAL) 47 | 48 | #endif // __FRULIB_SMBIOS_CHASSIS_H__ 49 | --------------------------------------------------------------------------------