fwupd/plugins/fastboot/fu-fastboot-device.c
Richard Hughes bf72d393fd Split up FuUsbDevice into ->open() and ->setup()
Before this change calling FuUsbDevice->open() opened the device, and
also unconditionally added various GUIDs and InstanceIDs which we
normally do in setup.
Then fu_device_setup() would call the FuSubclass->setup() vfunc which
would have no way of either opting out of the FuUsbDevice->setup()-like
behaviour, or controlling if the parent class ->setup is run before or
after the subclass setup.

Split up FuUsbDevice->open() into clear ->open() and ->setup() phases
and add the parent class calls where appropriate.

This means that ->setup() now behaves the same as all the other vfuncs.
2021-06-17 16:21:41 +01:00

737 lines
20 KiB
C

/*
* Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include <string.h>
#include <xmlb.h>
#include "fu-fastboot-device.h"
#define FASTBOOT_REMOVE_DELAY_RE_ENUMERATE 60000 /* ms */
#define FASTBOOT_TRANSACTION_TIMEOUT 1000 /* ms */
#define FASTBOOT_TRANSACTION_RETRY_MAX 600
#define FASTBOOT_EP_IN 0x81
#define FASTBOOT_EP_OUT 0x01
#define FASTBOOT_CMD_BUFSZ 64 /* bytes */
struct _FuFastbootDevice {
FuUsbDevice parent_instance;
gboolean secure;
guint blocksz;
guint8 intf_nr;
};
G_DEFINE_TYPE (FuFastbootDevice, fu_fastboot_device, FU_TYPE_USB_DEVICE)
static void
fu_fastboot_device_to_string (FuDevice *device, guint idt, GString *str)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
fu_common_string_append_kx (str, idt, "InterfaceNumber", self->intf_nr);
fu_common_string_append_kx (str, idt, "BlockSize", self->blocksz);
fu_common_string_append_kb (str, idt, "Secure", self->secure);
}
static gboolean
fu_fastboot_device_probe (FuDevice *device, GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
g_autoptr(GUsbInterface) intf = NULL;
/* find the correct fastboot interface */
intf = g_usb_device_get_interface (usb_device, 0xff, 0x42, 0x03, error);
if (intf == NULL)
return FALSE;
self->intf_nr = g_usb_interface_get_number (intf);
return TRUE;
}
static gboolean
fu_fastboot_device_open (FuDevice *device, GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
/* FuUsbDevice->open */
if (!FU_DEVICE_CLASS (fu_fastboot_device_parent_class)->open (device, error))
return FALSE;
if (!g_usb_device_claim_interface (usb_device, self->intf_nr,
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
g_prefix_error (error, "failed to claim interface: ");
return FALSE;
}
/* success */
return TRUE;
}
static void
fu_fastboot_buffer_dump (const gchar *title, const guint8 *buf, gsize sz)
{
if (g_getenv ("FWUPD_FASTBOOT_VERBOSE") == NULL)
return;
g_print ("%s (%" G_GSIZE_FORMAT "):\n", title, sz);
for (gsize i = 0; i < sz; i++) {
g_print ("%02x[%c] ", buf[i], g_ascii_isprint (buf[i]) ? buf[i] : '?');
if (i > 0 && (i + 1) % 256 == 0)
g_print ("\n");
}
g_print ("\n");
}
static gboolean
fu_fastboot_device_write (FuDevice *device, const guint8 *buf, gsize buflen, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
gboolean ret;
gsize actual_len = 0;
g_autofree guint8 *buf2 = NULL;
/* make mutable */
buf2 = fu_memdup_safe (buf, buflen, error);
if (buf2 == NULL)
return FALSE;
fu_fastboot_buffer_dump ("writing", buf, buflen);
ret = g_usb_device_bulk_transfer (usb_device,
FASTBOOT_EP_OUT,
buf2,
buflen,
&actual_len,
FASTBOOT_TRANSACTION_TIMEOUT,
NULL, error);
if (!ret) {
g_prefix_error (error, "failed to do bulk transfer: ");
return FALSE;
}
if (actual_len != buflen) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"only wrote %" G_GSIZE_FORMAT "bytes", actual_len);
return FALSE;
}
return TRUE;
}
static gboolean
fu_fastboot_device_writestr (FuDevice *device, const gchar *str, GError **error)
{
gsize buflen = strlen (str);
if (buflen > FASTBOOT_CMD_BUFSZ - 4) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"fastboot limits writes to %i bytes",
FASTBOOT_CMD_BUFSZ - 4);
return FALSE;
}
return fu_fastboot_device_write (device, (const guint8 *) str, buflen, error);
}
typedef enum {
FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
} FuFastbootDeviceReadFlags;
static gboolean
fu_fastboot_device_read (FuDevice *device,
gchar **str,
FuFastbootDeviceReadFlags flags,
GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
guint retries = 1;
/* these commands may return INFO or take some time to complete */
if (flags & FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL)
retries = FASTBOOT_TRANSACTION_RETRY_MAX;
for (guint i = 0; i < retries; i++) {
gboolean ret;
gsize actual_len = 0;
guint8 buf[FASTBOOT_CMD_BUFSZ] = { 0x00 };
g_autofree gchar *tmp = NULL;
g_autoptr(GError) error_local = NULL;
ret = g_usb_device_bulk_transfer (usb_device,
FASTBOOT_EP_IN,
buf,
sizeof(buf),
&actual_len,
FASTBOOT_TRANSACTION_TIMEOUT,
NULL, &error_local);
if (!ret) {
if (g_error_matches (error_local,
G_USB_DEVICE_ERROR,
G_USB_DEVICE_ERROR_TIMED_OUT)) {
g_debug ("ignoring %s", error_local->message);
continue;
}
g_propagate_prefixed_error (error,
g_steal_pointer (&error_local),
"failed to do bulk transfer: ");
return FALSE;
}
fu_fastboot_buffer_dump ("read", buf, actual_len);
if (actual_len < 4) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"only read %" G_GSIZE_FORMAT "bytes", actual_len);
return FALSE;
}
/* info */
tmp = g_strndup ((const gchar *) buf + 4, self->blocksz - 4);
if (memcmp (buf, "INFO", 4) == 0) {
if (g_strcmp0 (tmp, "erasing flash") == 0)
fu_device_set_status (device, FWUPD_STATUS_DEVICE_ERASE);
else if (g_strcmp0 (tmp, "writing flash") == 0)
fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
else
g_debug ("INFO returned unknown: %s", tmp);
continue;
}
/* success */
if (memcmp (buf, "OKAY", 4) == 0 || memcmp (buf, "DATA", 4) == 0) {
if (str != NULL)
*str = g_steal_pointer (&tmp);
return TRUE;
}
/* failure */
if (memcmp (buf, "FAIL", 4) == 0) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"failed to read response: %s", tmp);
return FALSE;
}
/* unknown failure */
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"failed to read response");
return FALSE;
}
/* we timed out a *lot* */
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"no response to read");
return FALSE;
}
static gboolean
fu_fastboot_device_getvar (FuDevice *device, const gchar *key, gchar **str, GError **error)
{
g_autofree gchar *tmp = g_strdup_printf ("getvar:%s", key);
if (!fu_fastboot_device_writestr (device, tmp, error))
return FALSE;
if (!fu_fastboot_device_read (device, str,
FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error)) {
g_prefix_error (error, "failed to getvar %s: ", key);
return FALSE;
}
return TRUE;
}
static gboolean
fu_fastboot_device_cmd (FuDevice *device, const gchar *cmd,
FuFastbootDeviceReadFlags flags, GError **error)
{
if (!fu_fastboot_device_writestr (device, cmd, error))
return FALSE;
if (!fu_fastboot_device_read (device, NULL, flags, error))
return FALSE;
return TRUE;
}
static gboolean
fu_fastboot_device_flash (FuDevice *device, const gchar *partition, GError **error)
{
g_autofree gchar *tmp = g_strdup_printf ("flash:%s", partition);
return fu_fastboot_device_cmd (device, tmp,
FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
error);
}
static gboolean
fu_fastboot_device_download (FuDevice *device, GBytes *fw, GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
gsize sz = g_bytes_get_size (fw);
g_autofree gchar *tmp = g_strdup_printf ("download:%08x", (guint) sz);
g_autoptr(GPtrArray) chunks = NULL;
/* tell the client the size of data to expect */
if (!fu_fastboot_device_cmd (device, tmp,
FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
error))
return FALSE;
/* send the data in chunks */
fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
chunks = fu_chunk_array_new_from_bytes (fw,
0x00, /* start addr */
0x00, /* page_sz */
self->blocksz);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index (chunks, i);
if (!fu_fastboot_device_write (device,
fu_chunk_get_data (chk),
fu_chunk_get_data_sz (chk),
error))
return FALSE;
fu_device_set_progress_full (device, (gsize) i, (gsize) chunks->len * 2);
}
if (!fu_fastboot_device_read (device, NULL,
FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, error))
return FALSE;
return TRUE;
}
static gboolean
fu_fastboot_device_setup (FuDevice *device, GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
g_autofree gchar *product = NULL;
g_autofree gchar *serialno = NULL;
g_autofree gchar *version = NULL;
g_autofree gchar *secure = NULL;
g_autofree gchar *version_bootloader = NULL;
/* FuUsbDevice->setup */
if (!FU_DEVICE_CLASS (fu_fastboot_device_parent_class)->setup (device, error))
return FALSE;
/* product */
if (!fu_fastboot_device_getvar (device, "product", &product, error))
return FALSE;
if (product != NULL && product[0] != '\0') {
g_autofree gchar *tmp = g_strdup_printf ("Fastboot %s", product);
fu_device_set_name (device, tmp);
}
/* fastboot API version */
if (!fu_fastboot_device_getvar (device, "version", &version, error))
return FALSE;
if (version != NULL && version[0] != '\0')
g_debug ("fastboot version=%s", version);
/* bootloader version */
if (!fu_fastboot_device_getvar (device, "version-bootloader", &version_bootloader, error))
return FALSE;
if (version_bootloader != NULL && version_bootloader[0] != '\0') {
fu_device_set_version_format (device, FWUPD_VERSION_FORMAT_PAIR);
fu_device_set_version_bootloader (device, version_bootloader);
}
/* serialno */
if (!fu_fastboot_device_getvar (device, "serialno", &serialno, error))
return FALSE;
if (serialno != NULL && serialno[0] != '\0')
fu_device_set_serial (device, serialno);
/* secure */
if (!fu_fastboot_device_getvar (device, "secure", &secure, error))
return FALSE;
if (secure != NULL && secure[0] != '\0')
self->secure = TRUE;
/* success */
return TRUE;
}
static gboolean
fu_fastboot_device_write_qfil_part (FuDevice *device,
FuArchive *archive,
XbNode *part,
GError **error)
{
GBytes *data;
const gchar *fn;
const gchar *partition;
/* not all partitions have images */
fn = xb_node_query_text (part, "img_name", NULL);
if (fn == NULL)
return TRUE;
/* find filename */
data = fu_archive_lookup_by_fn (archive, fn, error);
if (data == NULL)
return FALSE;
/* get the partition name */
partition = xb_node_query_text (part, "name", error);
if (partition == NULL)
return FALSE;
if (g_str_has_prefix (partition, "0:"))
partition += 2;
/* flash the partition */
if (!fu_fastboot_device_download (device, data, error))
return FALSE;
return fu_fastboot_device_flash (device, partition, error);
}
static gboolean
fu_fastboot_device_write_motorola_part (FuDevice *device,
FuArchive *archive,
XbNode *part,
GError **error)
{
const gchar *op = xb_node_get_attr (part, "operation");
/* oem */
if (g_strcmp0 (op, "oem") == 0) {
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"OEM commands are not supported");
return FALSE;
}
/* getvar */
if (g_strcmp0 (op, "getvar") == 0) {
const gchar *var = xb_node_get_attr (part, "var");
g_autofree gchar *tmp = NULL;
/* check required args */
if (var == NULL) {
tmp = xb_node_export (part, XB_NODE_EXPORT_FLAG_NONE, NULL);
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"required var for part: %s", tmp);
return FALSE;
}
/* just has to be non-empty */
if (!fu_fastboot_device_getvar (device, var, &tmp, error))
return FALSE;
if (tmp == NULL || tmp[0] == '\0') {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"failed to getvar %s", var);
return FALSE;
}
return TRUE;
}
/* erase */
if (g_strcmp0 (op, "erase") == 0) {
const gchar *partition = xb_node_get_attr (part, "partition");
g_autofree gchar *cmd = g_strdup_printf ("erase:%s", partition);
/* check required args */
if (partition == NULL) {
g_autofree gchar *tmp = NULL;
tmp = xb_node_export (part, XB_NODE_EXPORT_FLAG_NONE, NULL);
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"required partition for part: %s", tmp);
return FALSE;
}
/* erase the partition */
return fu_fastboot_device_cmd (device, cmd,
FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
error);
}
/* flash */
if (g_strcmp0 (op, "flash") == 0) {
GBytes *data;
const gchar *filename = xb_node_get_attr (part, "filename");
const gchar *partition = xb_node_get_attr (part, "partition");
struct {
GChecksumType kind;
const gchar *str;
} csum_kinds[] = {
{ G_CHECKSUM_MD5, "MD5" },
{ G_CHECKSUM_SHA1, "SHA1" },
{ 0, NULL }
};
/* check required args */
if (partition == NULL || filename == NULL) {
g_autofree gchar *tmp = NULL;
tmp = xb_node_export (part, XB_NODE_EXPORT_FLAG_NONE, NULL);
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"required partition and filename: %s", tmp);
return FALSE;
}
/* find filename */
data = fu_archive_lookup_by_fn (archive, filename, error);
if (data == NULL)
return FALSE;
/* checksum is optional */
for (guint i = 0; csum_kinds[i].str != NULL; i++) {
const gchar *csum;
g_autofree gchar *csum_actual = NULL;
/* not provided */
csum = xb_node_get_attr (part, csum_kinds[i].str);
if (csum == NULL)
continue;
/* check is valid */
csum_actual = g_compute_checksum_for_bytes (csum_kinds[i].kind, data);
if (g_strcmp0 (csum, csum_actual) != 0) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"%s invalid, expected %s, got %s",
filename, csum, csum_actual);
return FALSE;
}
}
/* flash the partition */
if (!fu_fastboot_device_download (device, data, error))
return FALSE;
return fu_fastboot_device_flash (device, partition, error);
}
/* dumb operation that doesn't expect a response */
if (g_strcmp0 (op, "boot") == 0 ||
g_strcmp0 (op, "continue") == 0 ||
g_strcmp0 (op, "reboot") == 0 ||
g_strcmp0 (op, "reboot-bootloader") == 0 ||
g_strcmp0 (op, "powerdown") == 0) {
return fu_fastboot_device_cmd (device, op,
FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
error);
}
/* unknown */
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"unknown operation %s", op);
return FALSE;
}
static gboolean
fu_fastboot_device_write_motorola (FuDevice *device,
FuArchive* archive,
GError **error)
{
GBytes *data;
g_autoptr(GPtrArray) parts = NULL;
g_autoptr(XbBuilder) builder = xb_builder_new ();
g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
g_autoptr(XbSilo) silo = NULL;
/* load the manifest of operations */
data = fu_archive_lookup_by_fn (archive, "flashfile.xml", error);
if (data == NULL)
return FALSE;
if (!xb_builder_source_load_bytes (source, data,
XB_BUILDER_SOURCE_FLAG_NONE, error))
return FALSE;
xb_builder_import_source (builder, source);
silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error);
if (silo == NULL)
return FALSE;
/* get all the operation parts */
parts = xb_silo_query (silo, "parts/part", 0, error);
if (parts == NULL)
return FALSE;
for (guint i = 0; i < parts->len; i++) {
XbNode *part = g_ptr_array_index (parts, i);
if (!fu_fastboot_device_write_motorola_part (device,
archive,
part,
error))
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_fastboot_device_write_qfil (FuDevice *device, FuArchive* archive, GError **error)
{
GBytes *data;
g_autoptr(GPtrArray) parts = NULL;
g_autoptr(XbBuilder) builder = xb_builder_new ();
g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
g_autoptr(XbSilo) silo = NULL;
/* load the manifest of operations */
data = fu_archive_lookup_by_fn (archive, "partition_nand.xml", error);
if (data == NULL)
return FALSE;
if (!xb_builder_source_load_bytes (source, data,
XB_BUILDER_SOURCE_FLAG_NONE, error))
return FALSE;
xb_builder_import_source (builder, source);
silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error);
if (silo == NULL)
return FALSE;
/* get all the operation parts */
parts = xb_silo_query (silo, "nandboot/partitions/partition", 0, error);
if (parts == NULL)
return FALSE;
for (guint i = 0; i < parts->len; i++) {
XbNode *part = g_ptr_array_index (parts, i);
if (!fu_fastboot_device_write_qfil_part (device,
archive,
part,
error))
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_fastboot_device_write_firmware (FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
g_autoptr(FuArchive) archive = NULL;
g_autoptr(GBytes) fw = NULL;
/* get default image */
fw = fu_firmware_get_bytes (firmware, error);
if (fw == NULL)
return FALSE;
/* decompress entire archive ahead of time */
archive = fu_archive_new (fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error);
if (archive == NULL)
return FALSE;
/* load the manifest of operations */
if (fu_archive_lookup_by_fn (archive, "partition_nand.xml", NULL) != NULL)
return fu_fastboot_device_write_qfil (device, archive, error);
if (fu_archive_lookup_by_fn (archive, "flashfile.xml", NULL) != NULL) {
return fu_fastboot_device_write_motorola (device,
archive,
error);
}
/* not supported */
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"manifest not supported");
return FALSE;
}
static gboolean
fu_fastboot_device_close (FuDevice *device, GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
/* we're done here */
if (!g_usb_device_release_interface (usb_device, self->intf_nr,
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
g_prefix_error (error, "failed to release interface: ");
return FALSE;
}
/* FuUsbDevice->close */
return FU_DEVICE_CLASS (fu_fastboot_device_parent_class)->close (device, error);
}
static gboolean
fu_fastboot_device_set_quirk_kv (FuDevice *device,
const gchar *key,
const gchar *value,
GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
/* load from quirks */
if (g_strcmp0 (key, "FastbootBlockSize") == 0) {
guint64 tmp = fu_common_strtoull (value);
if (tmp >= 0x40 && tmp < 0x100000) {
self->blocksz = tmp;
return TRUE;
}
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"invalid block size");
return FALSE;
}
/* failed */
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"quirk key not supported");
return FALSE;
}
static gboolean
fu_fastboot_device_attach (FuDevice *device, GError **error)
{
fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
if (!fu_fastboot_device_cmd (device, "reboot",
FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
error))
return FALSE;
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
return TRUE;
}
static void
fu_fastboot_device_init (FuFastbootDevice *self)
{
/* this is a safe default, even using USBv1 */
self->blocksz = 512;
fu_device_add_protocol (FU_DEVICE (self), "com.google.fastboot");
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_ADD_COUNTERPART_GUIDS);
fu_device_add_internal_flag (FU_DEVICE (self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID);
fu_device_set_remove_delay (FU_DEVICE (self), FASTBOOT_REMOVE_DELAY_RE_ENUMERATE);
}
static void
fu_fastboot_device_class_init (FuFastbootDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
klass_device->probe = fu_fastboot_device_probe;
klass_device->setup = fu_fastboot_device_setup;
klass_device->write_firmware = fu_fastboot_device_write_firmware;
klass_device->attach = fu_fastboot_device_attach;
klass_device->to_string = fu_fastboot_device_to_string;
klass_device->set_quirk_kv = fu_fastboot_device_set_quirk_kv;
klass_device->open = fu_fastboot_device_open;
klass_device->close = fu_fastboot_device_close;
}