fw update: library to create pldm fw package

Implement some helper functions to easily create a pldm fw package in a
few lines of user code.

This is helpful for unit testing the code update flow.

The package created is for testing purpose. The library is not
fully-featured or complete and should not be relied upon for anything
other than the tests in this repo.

https://github.com/openbmc/docs/blob/master/designs/code-update.md

Tested: next few patches in series

Change-Id: If899e5537a2e5ac641544ca80e8876d83549da28
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/bmc/meson.build b/bmc/meson.build
index 84de1c5..0ff1c17 100644
--- a/bmc/meson.build
+++ b/bmc/meson.build
@@ -260,7 +260,6 @@
 
 # If test coverage of source files within the root directory are wanted,
 # need to define and build the tests from here
-build_tests = get_option('tests')
 if not build_tests.disabled()
     gtest = dependency('gtest', main: true, disabler: true, required: build_tests)
     include_srcs = declare_dependency(sources: [
diff --git a/meson.build b/meson.build
index e160d5d..5c6c21c 100644
--- a/meson.build
+++ b/meson.build
@@ -60,4 +60,14 @@
 systemd = dependency('systemd')
 systemd_system_unit_dir = systemd.get_variable('systemdsystemunitdir')
 
+build_tests = get_option('tests')
+
 subdir('bmc')
+
+libpldm_dep = dependency('libpldm')
+
+common_include = include_directories('.')
+
+if not build_tests.disabled()
+  subdir('test')
+endif
diff --git a/test/create_package/component_image_info_area.cpp b/test/create_package/component_image_info_area.cpp
new file mode 100644
index 0000000..52c4cbd
--- /dev/null
+++ b/test/create_package/component_image_info_area.cpp
@@ -0,0 +1,70 @@
+#include <inttypes.h>
+#include <unistd.h>
+
+#include <cstring>
+
+/*
+ * componentLocationOffsetIndex  is for backfilling by the caller
+ */
+ssize_t create_pldm_component_image_info_area_v1_0_0(
+    uint8_t* b, ssize_t i, size_t component_image_size,
+    size_t& componentLocationOffsetIndex)
+{
+    // Component Image Count
+    b[i++] = 0x1;
+    b[i++] = 0x0;
+
+    // ComponentImageInformation (Table 5)
+    // (1 for each image)
+
+    // ComponentClassification
+    b[i++] = 0x1; // this is vendor selected value
+    b[i++] = 0x0;
+
+    // ComponentIdentifier
+    b[i++] = 0x1; // this is vendor selected value
+    b[i++] = 0x0;
+
+    // ComponentComparisonStamp
+    b[i++] = 0xff;
+    b[i++] = 0xff;
+    b[i++] = 0xff;
+    b[i++] = 0xff;
+
+    // ComponentOptions
+    b[i++] = 0x00;
+    b[i++] = 0x00;
+
+    // RequestedComponentActivationMethod
+    b[i++] = 0b100000; // AC Power Cycle
+    b[i++] = 0x0;
+
+    // ComponentLocationOffset
+    // (leave blank for now)
+    componentLocationOffsetIndex = i;
+    b[i++] = 0x0;
+    b[i++] = 0x0;
+    b[i++] = 0x0;
+    b[i++] = 0x0;
+
+    // ComponentSize
+    b[i++] = (component_image_size >> 0) & 0xff;
+    b[i++] = (component_image_size >> 8) & 0xff;
+    b[i++] = (component_image_size >> 16) & 0xff;
+    b[i++] = (component_image_size >> 24) & 0xff;
+
+    // ComponentVersionStringType
+    b[i++] = 0x1; // type = Ascii
+
+    const char* buf = (const char*)"mycompversion";
+    // ComponentVersionStringLength
+    b[i++] = strlen(buf);
+
+    // ComponentVersionString
+    for (ssize_t j = 0; j < (ssize_t)strlen(buf); j++)
+    {
+        b[i++] = buf[j];
+    }
+
+    return i;
+}
diff --git a/test/create_package/component_image_info_area.hpp b/test/create_package/component_image_info_area.hpp
new file mode 100644
index 0000000..3a24c20
--- /dev/null
+++ b/test/create_package/component_image_info_area.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <unistd.h>
+
+#include <cstdint>
+
+/*
+ * componentLocationOffsetIndex  is for backfilling by the caller
+ */
+ssize_t create_pldm_component_image_info_area_v1_0_0(
+    uint8_t* b, ssize_t i, size_t component_image_size,
+    size_t& componentLocationOffsetIndex);
diff --git a/test/create_package/create_pldm_fw_package.cpp b/test/create_package/create_pldm_fw_package.cpp
new file mode 100644
index 0000000..c4353bb
--- /dev/null
+++ b/test/create_package/create_pldm_fw_package.cpp
@@ -0,0 +1,145 @@
+
+#include "component_image_info_area.hpp"
+#include "firmware_device_id_area.hpp"
+#include "phosphor-logging/lg2.hpp"
+
+#include <inttypes.h>
+#include <libpldm/firmware_update.h>
+
+#include <cstring>
+#include <fstream>
+#include <limits>
+#include <random>
+
+std::unique_ptr<uint8_t[]> create_pldm_package_buffer(
+    const uint8_t* component_image, size_t component_image_size,
+    const std::optional<uint32_t>& optVendorIANA,
+    const std::optional<std::string>& optCompatible, size_t& size_out)
+{
+    const size_t size = 512 + component_image_size;
+    auto buffer = std::make_unique<uint8_t[]>(size);
+    uint8_t* b = buffer.get();
+    memset(b, 0, size);
+    ssize_t i = 0; // index
+
+    // populate uuid
+    // must be this value to align with
+    // https://github.com/openbmc/pldm/blob/master/fw-update/package_parser.cpp#L294
+    uint8_t uuid[PLDM_FWUP_UUID_LENGTH] = {
+        0xF0, 0x18, 0x87, 0x8C, 0xCB, 0x7D, 0x49, 0x43,
+        0x98, 0x00, 0xA0, 0x2F, 0x05, 0x9A, 0xCA, 0x02};
+    memcpy(b, uuid, PLDM_FWUP_UUID_LENGTH);
+
+    i += PLDM_FWUP_UUID_LENGTH;
+
+    // package header format revision
+    b[i++] = 0x01;
+    // must be 1 to align with
+    // https://github.com/openbmc/pldm/blob/master/fw-update/package_parser.cpp#L294
+
+    // package header size (leave space)
+    ssize_t package_header_size_offset = i;
+    i += 2;
+
+    // package release date time
+    // set timestamp as unknown value
+    b[i + 12] = 15;
+    i += PLDM_TIMESTAMP104_SIZE;
+
+    // component bitmap bit length
+    const uint16_t componentBitmapBitLength = 8;
+    b[i++] = componentBitmapBitLength;
+    b[i++] = 0x00;
+
+    // package_version_string_type
+    b[i++] = 0x01; // type = ASCII
+
+    const char* package_version_str = (const char*)"VersionString1";
+    // package version string length
+    b[i++] = strlen(package_version_str);
+
+    // package version string
+    for (size_t j = 0; j < strlen(package_version_str); j++)
+    {
+        b[i++] = package_version_str[j];
+    }
+
+    // --- Firmware Device Identification Area 1.0.0 ---
+
+    i = create_pldm_firmware_device_identification_area_v1_0_0(
+        b, i, optVendorIANA, optCompatible, componentBitmapBitLength);
+
+    // --- Component Image Information Area 1.0.0 ---
+    size_t componentLocationOffsetIndex;
+    i = create_pldm_component_image_info_area_v1_0_0(
+        b, i, component_image_size, componentLocationOffsetIndex);
+
+    // PackageHeaderChecksum (backfill later)
+    const size_t packageHeaderChecksumOffset = i;
+    i += 4;
+
+    // backfill the PackageHeaderSize
+    b[package_header_size_offset + 0] = (i >> 0) & 0xff;
+    b[package_header_size_offset + 1] = (i >> 8) & 0xff;
+
+    // backfill the ComponentLocationOffset
+    b[componentLocationOffsetIndex + 0] = (i >> 0) & 0xff;
+    b[componentLocationOffsetIndex + 1] = (i >> 8) & 0xff;
+    b[componentLocationOffsetIndex + 2] = (i >> 16) & 0xff;
+    b[componentLocationOffsetIndex + 3] = (i >> 24) & 0xff;
+
+    // backfill PackageHeaderChecksum
+    const uint32_t crc = crc32(b, packageHeaderChecksumOffset);
+    memcpy(b + packageHeaderChecksumOffset, &crc, 4);
+
+    // --- end of the package header ---
+
+    // write the component image
+    for (size_t j = 0; j < component_image_size; j++)
+    {
+        b[i++] = component_image[j];
+    }
+
+    lg2::debug("wrote {NBYTES} bytes for pldm update package", "NBYTES", i);
+
+    size_out = i;
+    return buffer;
+}
+
+static void create_pldm_package_file(std::ofstream& of,
+                                     const uint8_t* component_image,
+                                     size_t component_image_size)
+{
+    size_t size;
+    std::unique_ptr<uint8_t[]> buf =
+        create_pldm_package_buffer(component_image, component_image_size,
+                                   std::nullopt, std::nullopt, size);
+    uint8_t* b = buf.get();
+
+    of.write(reinterpret_cast<char*>(b), (long)size);
+}
+
+std::optional<std::string>
+    create_pldm_package(uint8_t* component_image, size_t component_image_size)
+{
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<> distrib(0, std::numeric_limits<int>::max());
+
+    std::string filename =
+        std::format("/tmp/pldm-package-{}.bin", distrib(gen));
+
+    std::ofstream of(filename, std::ofstream::out);
+
+    if (!of.good())
+    {
+        lg2::error("could not create file: {FILENAME}", "FILENAME", filename);
+        return std::nullopt;
+    }
+
+    create_pldm_package_file(of, component_image, component_image_size);
+
+    of.close();
+
+    return filename;
+}
diff --git a/test/create_package/create_pldm_fw_package.hpp b/test/create_package/create_pldm_fw_package.hpp
new file mode 100644
index 0000000..0d37c6a
--- /dev/null
+++ b/test/create_package/create_pldm_fw_package.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <inttypes.h>
+
+#include <memory>
+#include <optional>
+#include <string>
+
+std::optional<std::string>
+    create_pldm_package(uint8_t* component_image, size_t component_image_size);
+
+std::unique_ptr<uint8_t[]> create_pldm_package_buffer(
+    const uint8_t* component_image, size_t component_image_size,
+    const std::optional<uint32_t>& optVendorIANA,
+    const std::optional<std::string>& optCompatible, size_t& size_out);
diff --git a/test/create_package/firmware_device_id_area.cpp b/test/create_package/firmware_device_id_area.cpp
new file mode 100644
index 0000000..496340c
--- /dev/null
+++ b/test/create_package/firmware_device_id_area.cpp
@@ -0,0 +1,174 @@
+#include <inttypes.h>
+#include <libpldm/firmware_update.h>
+
+#include <cstring>
+#include <optional>
+#include <string>
+#include <vector>
+
+static ssize_t create_pldm_firmware_device_descriptor_v1_0_0(
+    uint8_t* b, ssize_t i, uint16_t descType, std::vector<uint8_t>& data)
+{
+    // DescriptorType, Table 7. (0x0001 == IANA Enterprise ID, 4 bytes)
+    b[i++] = (descType >> 0) & 0xff;
+    b[i++] = (descType >> 8) & 0xff;
+
+    // DescriptorLength
+    b[i++] = (data.size() >> 0) & 0xff;
+    b[i++] = (data.size() >> 8) & 0xff;
+
+    // DescriptorData
+    for (uint8_t v : data)
+    {
+        b[i++] = v;
+    }
+
+    return i;
+}
+
+static ssize_t create_pldm_firmware_device_vendor_defined_descriptor_v1_0_0(
+    uint8_t* b, ssize_t i, std::vector<uint8_t>& data)
+{
+    const uint16_t descType = 0xffff; // vendor defined
+
+    // DescriptorType, Table 7.
+    b[i++] = (descType >> 0) & 0xff;
+    b[i++] = (descType >> 8) & 0xff;
+
+    // DescriptorLength
+    const uint16_t length = data.size() + 3; // data and the additional fields
+    b[i++] = (length >> 0) & 0xff;
+    b[i++] = (length >> 8) & 0xff;
+
+    // VendorDefinedDescriptorTitleStringType
+    b[i++] = 0x1; // type 1 = ascii
+
+    // VendorDefinedDescriptorTitleStringLength
+    b[i++] = data.size();
+
+    // VendorDefinedDescriptorTitleString
+    for (uint8_t v : data)
+    {
+        b[i++] = v;
+    }
+
+    // VendorDefinedDescriptorData
+    b[i++] = 0x00;
+
+    return i;
+}
+
+static ssize_t create_pldm_firmware_device_descriptors_v1_0_0(
+    uint8_t* b, ssize_t i, const std::optional<uint32_t>& optVendorIANA,
+    const std::optional<std::string>& optCompatible, size_t* actual_count)
+{
+    // RecordDescriptiors. The initial descriptor is restricted.
+    // In our case we use iana for the first descriptor.
+
+    std::vector<uint8_t> descriptorData = {0xaf, 0xaf, 0xaf, 0xaf};
+
+    if (optVendorIANA.has_value())
+    {
+        uint32_t v = optVendorIANA.value();
+        descriptorData[0] = (v >> 0) & 0xff;
+        descriptorData[1] = (v >> 8) & 0xff;
+        descriptorData[2] = (v >> 16) & 0xff;
+        descriptorData[3] = (v >> 24) & 0xff;
+    }
+
+    i = create_pldm_firmware_device_descriptor_v1_0_0(b, i, 0x1,
+                                                      descriptorData);
+
+    *actual_count = 1;
+
+    if (optCompatible.has_value())
+    {
+        std::string comp = optCompatible.value();
+        std::vector<uint8_t> compatible = {};
+        for (char c : comp)
+        {
+            compatible.push_back((uint8_t)c);
+        }
+        i = create_pldm_firmware_device_vendor_defined_descriptor_v1_0_0(
+            b, i, compatible);
+
+        *actual_count = 2;
+    }
+
+    return i;
+}
+
+ssize_t create_pldm_firmware_device_identification_record(
+    uint8_t* b, ssize_t i, const std::optional<uint32_t>& optVendorIANA,
+    const std::optional<std::string>& optCompatible,
+    uint16_t componentBitmapBitLength)
+{
+    const ssize_t startIndex = i;
+    // RecordLength, backfill later
+    const size_t recordLengthOffset = i;
+    b[i++] = 0;
+    b[i++] = 0;
+
+    // DescriptorCount (backfill later)
+    const size_t descriptorCountOffset = i;
+    b[i++] = 0;
+
+    // DeviceUpdateOptionFlags
+    b[i++] = 0;
+    b[i++] = 0;
+    b[i++] = 0;
+    b[i++] = 0;
+
+    // ComponentImageSetVersionStringType
+    b[i++] = 1; // type = Ascii
+
+    const char* str = (const char*)"compimagesetversion1";
+    // ComponentImageSetVersionStringLength
+    b[i++] = strlen(str);
+
+    // FirmwareDevicePackageDataLength
+    b[i++] = 0x0;
+    b[i++] = 0x0;
+
+    // ApplicableComponents
+    for (int j = 0; j < (componentBitmapBitLength / 8); j++)
+    {
+        b[i++] = 0x1;
+        // the first and only component image does apply to this device
+    }
+
+    // ComponentSetVersionString
+    for (size_t j = 0; j < strlen(str); j++)
+    {
+        b[i++] = str[j];
+    }
+
+    // RecordDescriptiors. The initial descriptor is restricted.
+    size_t actual_count = 0;
+    i = create_pldm_firmware_device_descriptors_v1_0_0(
+        b, i, optVendorIANA, optCompatible, &actual_count);
+
+    // backfill DescriptorCount
+    b[descriptorCountOffset] = actual_count;
+
+    // FirmwareDevicePackageData (optional, we make it empty)
+
+    // backfill RecordLength
+    const ssize_t recordLength = i - startIndex;
+    b[recordLengthOffset + 0] = (recordLength >> 0) & 0xff;
+    b[recordLengthOffset + 1] = (recordLength >> 8) & 0xff;
+
+    return i;
+}
+
+ssize_t create_pldm_firmware_device_identification_area_v1_0_0(
+    uint8_t* b, ssize_t i, const std::optional<uint32_t>& optVendorIANA,
+    const std::optional<std::string>& optCompatible,
+    uint16_t componentBitmapBitLength)
+{
+    // Device ID Record Count
+    b[i++] = 1;
+
+    return create_pldm_firmware_device_identification_record(
+        b, i, optVendorIANA, optCompatible, componentBitmapBitLength);
+}
diff --git a/test/create_package/firmware_device_id_area.hpp b/test/create_package/firmware_device_id_area.hpp
new file mode 100644
index 0000000..253ca78
--- /dev/null
+++ b/test/create_package/firmware_device_id_area.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <unistd.h>
+
+#include <cstdint>
+#include <optional>
+#include <string>
+
+ssize_t create_pldm_firmware_device_identification_record(
+    uint8_t* b, ssize_t i, const std::optional<uint32_t>& optVendorIANA,
+    const std::optional<std::string>& optCompatible,
+    uint16_t componentBitmapBitLength);
+
+ssize_t create_pldm_firmware_device_identification_area_v1_0_0(
+    uint8_t* b, ssize_t i, const std::optional<uint32_t>& optVendorIANA,
+    const std::optional<std::string>& optCompatible,
+    uint16_t componentBitmapBitLength);
diff --git a/test/create_package/meson.build b/test/create_package/meson.build
new file mode 100644
index 0000000..44cc9fe
--- /dev/null
+++ b/test/create_package/meson.build
@@ -0,0 +1,12 @@
+
+libpldmcreatepkg = static_library('pldmcreatepkg',
+  'create_pldm_fw_package.cpp',
+  'firmware_device_id_area.cpp',
+  'component_image_info_area.cpp',
+  include_directories: ['.'],
+  dependencies: [
+    phosphor_logging_dep,
+    libpldm_dep,
+  ],
+  install: false,
+)
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..20b6879
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1 @@
+subdir('create_package')