fwupd/libfwupdplugin/fu-fdt-firmware.c
Richard Hughes eb30233b35 Improve FDT parsing compatibility
Newer versions of dtc agressively pack the string table, using *any* matching
suffix rather than whole strings. This is specification compliant, but breaks
if you use the offset as a hash key as a [premature] optimization.

Just use the string table as a flat block of memory rather than pre-parsing it.

This fixes reading the system FDT on new OpenBMC builds.
2023-01-18 07:59:45 +00:00

729 lines
19 KiB
C

/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#define G_LOG_DOMAIN "FuFirmware"
#include "config.h"
#include "fu-byte-array.h"
#include "fu-bytes.h"
#include "fu-common.h"
#include "fu-crc.h"
#include "fu-dump.h"
#include "fu-fdt-firmware.h"
#include "fu-fdt-image.h"
#include "fu-mem.h"
/**
* FuFdtFirmware:
*
* A Flattened DeviceTree firmware image.
*
* Documented:
* https://devicetree-specification.readthedocs.io/en/latest/chapter5-flattened-format.html
*
* See also: [class@FuFirmware]
*/
typedef struct {
guint32 cpuid;
} FuFdtFirmwarePrivate;
G_DEFINE_TYPE_WITH_PRIVATE(FuFdtFirmware, fu_fdt_firmware, FU_TYPE_FIRMWARE)
#define GET_PRIVATE(o) (fu_fdt_firmware_get_instance_private(o))
typedef struct __attribute__((packed)) {
guint32 magic;
guint32 totalsize;
guint32 off_dt_struct;
guint32 off_dt_strings;
guint32 off_mem_rsvmap;
guint32 version;
guint32 last_comp_version;
guint32 boot_cpuid_phys;
guint32 size_dt_strings;
guint32 size_dt_struct;
} FuFdtHeader;
typedef struct __attribute__((packed)) {
guint64 address;
guint64 size;
} FuFdtReserveEntry;
typedef struct __attribute__((packed)) {
guint32 len;
guint32 nameoff;
} FuFdtProp;
#define FDT_MAGIC 0xD00DFEED
#define FDT_BEGIN_NODE 0x00000001
#define FDT_END_NODE 0x00000002
#define FDT_PROP 0x00000003
#define FDT_NOP 0x00000004
#define FDT_END 0x00000009
#define FDT_LAST_COMP_VERSION 2
#define FDT_DEPTH_MAX 128
static GString *
fu_string_new_safe(const guint8 *buf, gsize bufsz, gsize offset, GError **error)
{
g_autoptr(GString) str = g_string_new(NULL);
for (gsize i = offset; i < bufsz; i++) {
if (buf[i] == '\0')
return g_steal_pointer(&str);
g_string_append_c(str, (gchar)buf[i]);
}
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"buffer not NULL terminated");
return NULL;
}
static void
fu_fdt_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn)
{
FuFdtFirmware *self = FU_FDT_FIRMWARE(firmware);
FuFdtFirmwarePrivate *priv = GET_PRIVATE(self);
fu_xmlb_builder_insert_kx(bn, "cpuid", priv->cpuid);
}
/**
* fu_fdt_firmware_get_cpuid:
* @self: a #FuFdtFirmware
*
* Gets the CPUID.
*
* Returns: integer
*
* Since: 1.8.2
**/
guint32
fu_fdt_firmware_get_cpuid(FuFdtFirmware *self)
{
FuFdtFirmwarePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_FDT_FIRMWARE(self), 0x0);
return priv->cpuid;
}
/**
* fu_fdt_firmware_set_cpuid:
* @self: a #FuFdtFirmware
* @cpuid: integer value
*
* Sets the CPUID.
*
* Since: 1.8.2
**/
void
fu_fdt_firmware_set_cpuid(FuFdtFirmware *self, guint32 cpuid)
{
FuFdtFirmwarePrivate *priv = GET_PRIVATE(self);
g_return_if_fail(FU_IS_FDT_FIRMWARE(self));
priv->cpuid = cpuid;
}
/**
* fu_fdt_firmware_get_image_by_path:
* @self: a #FuFdtFirmware
* @path: ID path, e.g. `/images/firmware-1`
* @error: (nullable): optional return location for an error
*
* Gets the FDT image for a specific path.
*
* Returns: (transfer full): a #FuFirmware, or %NULL
*
* Since: 1.8.2
**/
FuFdtImage *
fu_fdt_firmware_get_image_by_path(FuFdtFirmware *self, const gchar *path, GError **error)
{
g_auto(GStrv) paths = NULL;
g_autoptr(FuFirmware) img_current = g_object_ref(FU_FIRMWARE(self));
g_return_val_if_fail(FU_IS_FDT_FIRMWARE(self), NULL);
g_return_val_if_fail(path != NULL, NULL);
g_return_val_if_fail(path[0] != '\0', NULL);
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
paths = g_strsplit(path, "/", -1);
for (guint i = 0; paths[i] != NULL; i++) {
const gchar *id = paths[i];
g_autoptr(FuFirmware) img_tmp = NULL;
/* special case for empty */
if (id[0] == '\0')
id = NULL;
img_tmp = fu_firmware_get_image_by_id(img_current, id, error);
if (img_tmp == NULL)
return NULL;
g_set_object(&img_current, img_tmp);
}
/* success */
return FU_FDT_IMAGE(g_steal_pointer(&img_current));
}
static gboolean
fu_fdt_firmware_parse_dt_struct(FuFdtFirmware *self, GBytes *fw, GBytes *strtab, GError **error)
{
gsize bufsz = 0;
gsize offset = 0;
guint depth = 0;
gboolean has_end = FALSE;
const guint8 *buf = g_bytes_get_data(fw, &bufsz);
g_autoptr(FuFirmware) firmware_current = g_object_ref(FU_FIRMWARE(self));
/* debug */
if (g_getenv("FU_FDT_FIRMWARE_VERBOSE") != NULL)
fu_dump_bytes(G_LOG_DOMAIN, "dt_struct", fw);
/* parse */
while (offset < bufsz) {
guint32 token = 0;
/* read tag from aligned offset */
offset = fu_common_align_up(offset, FU_FIRMWARE_ALIGNMENT_4);
if (!fu_memread_uint32_safe(buf, bufsz, offset, &token, G_BIG_ENDIAN, error))
return FALSE;
if (g_getenv("FU_FDT_FIRMWARE_VERBOSE") != NULL)
g_debug("token: 0x%x", token);
offset += sizeof(guint32);
/* nothing to do */
if (token == FDT_NOP)
continue;
/* END */
if (token == FDT_END) {
if (firmware_current != FU_FIRMWARE(self)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"got END with unclosed node");
return FALSE;
}
has_end = TRUE;
break;
}
/* BEGIN NODE */
if (token == FDT_BEGIN_NODE) {
g_autoptr(GString) str = NULL;
g_autoptr(FuFirmware) image = NULL;
/* sanity check */
if (depth++ > FDT_DEPTH_MAX) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"node depth exceeded maximum: 0x%x",
(guint)FDT_DEPTH_MAX);
return FALSE;
}
str = fu_string_new_safe(buf, bufsz, offset, error);
if (str == NULL)
return FALSE;
offset += str->len + 1;
image = fu_fdt_image_new();
if (str->len > 0)
fu_firmware_set_id(image, str->str);
fu_firmware_set_offset(image, offset);
fu_firmware_add_image(firmware_current, image);
g_set_object(&firmware_current, image);
continue;
}
/* END NODE */
if (token == FDT_END_NODE) {
if (firmware_current == FU_FIRMWARE(self)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"got END NODE with no node to end");
return FALSE;
}
g_set_object(&firmware_current, fu_firmware_get_parent(firmware_current));
if (depth > 0)
depth--;
continue;
}
/* PROP */
if (token == FDT_PROP) {
guint32 prop_len = 0;
guint32 prop_nameoff = 0;
g_autoptr(GBytes) blob = NULL;
g_autoptr(GString) str = NULL;
/* sanity check */
if (firmware_current == FU_FIRMWARE(self)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"got PROP with unopen node");
return FALSE;
}
/* parse */
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtProp, len),
&prop_len,
G_BIG_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtProp, nameoff),
&prop_nameoff,
G_BIG_ENDIAN,
error))
return FALSE;
offset += sizeof(FuFdtProp);
/* add property */
str = fu_string_new_safe(g_bytes_get_data(strtab, NULL),
g_bytes_get_size(strtab),
prop_nameoff,
error);
if (str == NULL) {
g_prefix_error(error, "invalid strtab offset 0x%x: ", prop_nameoff);
return FALSE;
}
blob = fu_bytes_new_offset(fw, offset, prop_len, error);
if (blob == NULL)
return FALSE;
fu_fdt_image_set_attr(FU_FDT_IMAGE(firmware_current), str->str, blob);
offset += prop_len;
continue;
}
/* unknown token */
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"invalid token 0x%x @0%x",
token,
(guint)offset);
return FALSE;
}
/* did not see FDT_END */
if (!has_end) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"did not see FDT_END");
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_fdt_firmware_parse_mem_rsvmap(FuFdtFirmware *self,
const guint8 *buf,
gsize bufsz,
gsize offset,
GError **error)
{
/* parse */
for (; offset < bufsz; offset += sizeof(FuFdtReserveEntry)) {
guint64 address = 0;
guint64 size = 0;
if (!fu_memread_uint64_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtReserveEntry, address),
&address,
G_BIG_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint64_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtReserveEntry, size),
&size,
G_BIG_ENDIAN,
error))
return FALSE;
if (g_getenv("FU_FDT_FIRMWARE_VERBOSE") != NULL)
g_debug("mem_rsvmap: 0x%x, 0x%x", (guint)address, (guint)size);
if (address == 0x0 && size == 0x0)
break;
}
/* success */
return TRUE;
}
static gboolean
fu_fdt_firmware_check_magic(FuFirmware *firmware, GBytes *fw, gsize offset, GError **error)
{
guint32 magic = 0;
if (!fu_memread_uint32_safe(g_bytes_get_data(fw, NULL),
g_bytes_get_size(fw),
offset + G_STRUCT_OFFSET(FuFdtHeader, magic),
&magic,
G_BIG_ENDIAN,
error)) {
g_prefix_error(error, "failed to read magic: ");
return FALSE;
}
if (magic != FDT_MAGIC) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid magic 0x%x, expected 0x%x",
magic,
(guint)FDT_MAGIC);
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_fdt_firmware_parse(FuFirmware *firmware,
GBytes *fw,
gsize offset,
FwupdInstallFlags flags,
GError **error)
{
FuFdtFirmware *self = FU_FDT_FIRMWARE(firmware);
FuFdtFirmwarePrivate *priv = GET_PRIVATE(self);
guint32 totalsize = 0;
gsize bufsz = 0;
guint32 off_dt_struct = 0;
guint32 off_dt_strings = 0;
guint32 off_mem_rsvmap = 0;
guint32 version = 0;
guint32 last_comp_version = 0;
guint32 size_dt_strings = 0;
guint32 size_dt_struct = 0;
const guint8 *buf = g_bytes_get_data(fw, &bufsz);
/* sanity check */
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, totalsize),
&totalsize,
G_BIG_ENDIAN,
error))
return FALSE;
if (totalsize > bufsz) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"truncated image, got 0x%x, expected >= 0x%x",
(guint)bufsz,
(guint)totalsize);
return FALSE;
}
fu_firmware_set_size(firmware, totalsize);
/* read header */
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, off_dt_strings),
&off_dt_strings,
G_BIG_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, size_dt_strings),
&size_dt_strings,
G_BIG_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, off_dt_struct),
&off_dt_struct,
G_BIG_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, size_dt_struct),
&size_dt_struct,
G_BIG_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, off_mem_rsvmap),
&off_mem_rsvmap,
G_BIG_ENDIAN,
error))
return FALSE;
if (off_mem_rsvmap != 0x0) {
if (!fu_fdt_firmware_parse_mem_rsvmap(self,
buf,
bufsz,
offset + off_mem_rsvmap,
error))
return FALSE;
}
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, boot_cpuid_phys),
&priv->cpuid,
G_BIG_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, last_comp_version),
&last_comp_version,
G_BIG_ENDIAN,
error))
return FALSE;
if (last_comp_version < FDT_LAST_COMP_VERSION) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"invalid header version, got 0x%x, expected >= 0x%x",
(guint)last_comp_version,
(guint)FDT_LAST_COMP_VERSION);
return FALSE;
}
if (!fu_memread_uint32_safe(buf,
bufsz,
offset + G_STRUCT_OFFSET(FuFdtHeader, version),
&version,
G_BIG_ENDIAN,
error))
return FALSE;
fu_firmware_set_version_raw(firmware, version);
/* parse device tree struct */
if (size_dt_struct != 0x0 && size_dt_strings != 0x0) {
g_autoptr(GBytes) dt_strings = NULL;
g_autoptr(GBytes) dt_struct = NULL;
dt_strings =
fu_bytes_new_offset(fw, offset + off_dt_strings, size_dt_strings, error);
if (dt_strings == NULL)
return FALSE;
dt_struct = fu_bytes_new_offset(fw, offset + off_dt_struct, size_dt_struct, error);
if (dt_struct == NULL)
return FALSE;
if (!fu_fdt_firmware_parse_dt_struct(self, dt_struct, dt_strings, error))
return FALSE;
}
/* success */
return TRUE;
}
typedef struct {
GByteArray *dt_strings;
GByteArray *dt_struct;
GHashTable *strtab;
} FuFdtFirmwareBuildHelper;
static guint32
fu_fdt_firmware_append_to_strtab(FuFdtFirmwareBuildHelper *helper, const gchar *key)
{
gpointer tmp = NULL;
guint32 offset;
/* already exists */
if (g_hash_table_lookup_extended(helper->strtab, key, NULL, &tmp))
return GPOINTER_TO_UINT(tmp);
if (g_getenv("FU_FDT_FIRMWARE_VERBOSE") != NULL)
g_debug("adding strtab: %s", key);
offset = helper->dt_strings->len;
g_byte_array_append(helper->dt_strings, (const guint8 *)key, strlen(key));
fu_byte_array_append_uint8(helper->dt_strings, 0x0);
g_hash_table_insert(helper->strtab, g_strdup(key), GUINT_TO_POINTER(offset));
return offset;
}
static gboolean
fu_fdt_firmware_write_image(FuFdtImage *img,
FuFdtFirmwareBuildHelper *helper,
guint depth,
GError **error)
{
const gchar *id = fu_firmware_get_id(FU_FIRMWARE(img));
g_autoptr(GPtrArray) images = fu_firmware_get_images(FU_FIRMWARE(img));
g_autoptr(GPtrArray) attrs = fu_fdt_image_get_attrs(img);
/* sanity check */
if (depth > 0 && id == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"child FuFdtImage requires ID");
return FALSE;
}
/* BEGIN_NODE, ID, NUL */
fu_byte_array_append_uint32(helper->dt_struct, FDT_BEGIN_NODE, G_BIG_ENDIAN);
if (id != NULL) {
g_byte_array_append(helper->dt_struct, (const guint8 *)id, strlen(id) + 1);
} else {
fu_byte_array_append_uint8(helper->dt_struct, 0x0);
}
fu_byte_array_align_up(helper->dt_struct, FU_FIRMWARE_ALIGNMENT_4, 0x0);
/* write properties */
for (guint i = 0; i < attrs->len; i++) {
const gchar *key = g_ptr_array_index(attrs, i);
g_autoptr(GBytes) blob = NULL;
blob = fu_fdt_image_get_attr(img, key, error);
if (blob == NULL)
return FALSE;
fu_byte_array_append_uint32(helper->dt_struct, FDT_PROP, G_BIG_ENDIAN);
fu_byte_array_append_uint32(helper->dt_struct,
g_bytes_get_size(blob),
G_BIG_ENDIAN);
fu_byte_array_append_uint32(helper->dt_struct,
fu_fdt_firmware_append_to_strtab(helper, key),
G_BIG_ENDIAN);
fu_byte_array_append_bytes(helper->dt_struct, blob);
fu_byte_array_align_up(helper->dt_struct, FU_FIRMWARE_ALIGNMENT_4, 0x0);
}
/* write children, recursively */
for (guint i = 0; i < images->len; i++) {
FuFdtImage *img_child = g_ptr_array_index(images, i);
if (!fu_fdt_firmware_write_image(img_child, helper, depth + 1, error))
return FALSE;
}
/* END_NODE */
fu_byte_array_append_uint32(helper->dt_struct, FDT_END_NODE, G_BIG_ENDIAN);
return TRUE;
}
static GBytes *
fu_fdt_firmware_write(FuFirmware *firmware, GError **error)
{
FuFdtFirmware *self = FU_FDT_FIRMWARE(firmware);
FuFdtFirmwarePrivate *priv = GET_PRIVATE(self);
guint32 off_dt_struct;
guint32 off_dt_strings;
guint32 off_mem_rsvmap;
guint32 totalsize;
g_autoptr(GByteArray) buf = g_byte_array_new();
g_autoptr(GByteArray) dt_strings = g_byte_array_new();
g_autoptr(GByteArray) dt_struct = g_byte_array_new();
g_autoptr(GByteArray) mem_rsvmap = g_byte_array_new();
g_autoptr(GHashTable) strtab = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware);
FuFdtFirmwareBuildHelper helper = {
.dt_strings = dt_strings,
.dt_struct = dt_struct,
.strtab = strtab,
};
/* mem_rsvmap */
off_mem_rsvmap = fu_common_align_up(sizeof(FuFdtHeader), FU_FIRMWARE_ALIGNMENT_4);
fu_byte_array_append_uint64(mem_rsvmap, 0x0, G_BIG_ENDIAN);
fu_byte_array_append_uint64(mem_rsvmap, 0x0, G_BIG_ENDIAN);
/* dt_struct */
off_dt_struct =
fu_common_align_up(off_mem_rsvmap + mem_rsvmap->len, FU_FIRMWARE_ALIGNMENT_4);
/* only one root node supported */
if (images->len != 1) {
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "no root node");
return NULL;
}
if (!fu_fdt_firmware_write_image(FU_FDT_IMAGE(g_ptr_array_index(images, 0)),
&helper,
0,
error))
return NULL;
fu_byte_array_append_uint32(dt_struct, FDT_END, G_BIG_ENDIAN);
/* dt_strings */
off_dt_strings =
fu_common_align_up(off_dt_struct + dt_struct->len, FU_FIRMWARE_ALIGNMENT_4);
/* write header */
totalsize = off_dt_strings + dt_strings->len;
fu_byte_array_append_uint32(buf, FDT_MAGIC, G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, totalsize, G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, off_dt_struct, G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, off_dt_strings, G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, off_mem_rsvmap, G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, fu_firmware_get_version_raw(firmware), G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, FDT_LAST_COMP_VERSION, G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, priv->cpuid, G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, dt_strings->len, G_BIG_ENDIAN);
fu_byte_array_append_uint32(buf, dt_struct->len, G_BIG_ENDIAN);
fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_4, 0x0);
/* write mem_rsvmap, dt_struct, dt_strings */
g_byte_array_append(buf, mem_rsvmap->data, mem_rsvmap->len);
fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_4, 0x0);
g_byte_array_append(buf, dt_struct->data, dt_struct->len);
fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_4, 0x0);
g_byte_array_append(buf, dt_strings->data, dt_strings->len);
fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_4, 0x0);
/* success */
return g_byte_array_free_to_bytes(g_steal_pointer(&buf));
}
static gboolean
fu_fdt_firmware_build(FuFirmware *firmware, XbNode *n, GError **error)
{
FuFdtFirmware *self = FU_FDT_FIRMWARE(firmware);
FuFdtFirmwarePrivate *priv = GET_PRIVATE(self);
guint64 tmp;
/* optional properties */
tmp = xb_node_query_text_as_uint(n, "cpuid", NULL);
if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32)
priv->cpuid = tmp;
/* success */
return TRUE;
}
static void
fu_fdt_firmware_init(FuFdtFirmware *self)
{
g_type_ensure(FU_TYPE_FDT_IMAGE);
fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID);
}
static void
fu_fdt_firmware_class_init(FuFdtFirmwareClass *klass)
{
FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
klass_firmware->check_magic = fu_fdt_firmware_check_magic;
klass_firmware->export = fu_fdt_firmware_export;
klass_firmware->parse = fu_fdt_firmware_parse;
klass_firmware->write = fu_fdt_firmware_write;
klass_firmware->build = fu_fdt_firmware_build;
}
/**
* fu_fdt_firmware_new:
*
* Creates a new #FuFirmware of sub type FDT
*
* Since: 1.8.2
**/
FuFirmware *
fu_fdt_firmware_new(void)
{
return FU_FIRMWARE(g_object_new(FU_TYPE_FDT_FIRMWARE, NULL));
}