Add support for platform capability descriptors so devices can set quirks

This feature adds support for platform capability BOS descriptors which allows
the device itself to ship quirk data.

Use `sudo fwupdtool get-devices --save-backends=FILENAME` to save fake backend
devices to a file. This allows easy creation of self tests that do not require
physical hardware.
This commit is contained in:
Richard Hughes 2022-09-05 16:56:48 +01:00
parent a8017e0a4f
commit bfebede490
28 changed files with 1515 additions and 3 deletions

View File

@ -329,6 +329,14 @@ def _build(bld: Builder) -> None:
"PACKAGE_VERSION": "0.0.0",
},
)
bld.write_header(
"libfwupdplugin/fu-version.h",
{
"FU_MAJOR_VERSION": 0,
"FU_MINOR_VERSION": 0,
"FU_MICRO_VERSION": 0,
},
)
# libfwupd + libfwupdplugin
built_objs: List[str] = []

75
contrib/generate-ds20.py Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/python3
# pylint: disable=invalid-name,missing-docstring
#
# Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
#
# SPDX-License-Identifier: LGPL-2.1+
#
# pylint: disable=consider-using-f-string
import sys
import argparse
import configparser
import base64
from typing import List
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-b",
"--bufsz",
type=int,
help="Buffer size in bytes",
)
parser.add_argument("-i", "--instance-id", type=str, help="Device instance ID")
parser.add_argument("filename", action="store", type=str, help="Quirk filename")
args = parser.parse_args()
if not args.bufsz:
parser.print_help()
sys.exit(1)
config = configparser.ConfigParser()
config.optionxform = str
try:
config.read(args.filename)
except configparser.MissingSectionHeaderError:
print("Not a quirk file")
sys.exit(1)
# fall back to the default if there is only one device in the quirk file
if not args.instance_id:
sections = config.sections()
if len(sections) != 1:
print("Multiple devices found, use --instance-id to choose between:")
for section in sections:
print("{}".format(section))
sys.exit(1)
args.instance_id = sections[0]
# create the smallest kv store possible
lines: List[str] = []
try:
for key in config[args.instance_id]:
if key in ["Inhibit", "Issue"]:
print("WARNING: skipping key {}".format(key))
continue
value = config[args.instance_id][key]
lines.append("{}={}".format(key, value))
except KeyError:
print("No {} section".format(args.instance_id))
sys.exit(1)
# pad to the buffer size
buf: bytes = "\n".join(lines).encode()
if len(buf) > args.bufsz:
print("Quirk data is larger than bufsz")
sys.exit(1)
buf = buf.ljust(args.bufsz, b"\0")
# success
print("DS20 descriptor control transfer data:")
print(" ".join(["{:02x}".format(val) for val in list(buf)]))
print(base64.b64encode(buf).decode())

View File

@ -62,6 +62,7 @@ _fwupdtool_opts=(
'--no-safety-check'
'--ignore-checksum'
'--ignore-vid-pid'
'--save-backends'
)
_show_filters()

135
docs/ds20.md Normal file
View File

@ -0,0 +1,135 @@
# fwupd BOS DS20 Specification
## Introduction
When `fwupd` starts it enumerates all PCI and USB hardware and generates *Instance IDs* based on the reported vendor and product so that it can match the device to a plugin.
The plugin knows how to communicate with the device (for instance using vendor-specific USB control transfers) and also knows how to parse the firmware and deliver it to the device.
Before a vendor can ship a firmware on the LVFS to a machine using Linux (or ChromeOS) the fwupd package often must be updated so that it knows what plugin to use for that specific device.
At this point other overridden [quirk data](https://fwupd.github.io/libfwupdplugin/class.Quirks.html) can also be set. For instance, the icon, long summary or even per-plugin flags that change device behavior.
For example `colorhug.quirk`:
[USB\VID_273F&PID_1001]
Plugin = colorhug
Flags = self-recovery
Icon = colorimeter-colorhug
Updating the fwupd binary package might take anywhere from a few weeks to several years due to various Linux distribution policies.
Clearly this isn't good when the majority of firmware updates are distributed to address security issues.
One suggestion would be to put this quirk information into the existing update metadata provided by the configured remote, but this has several problems:
* The daemon needs to *enumerate* hardware that does not have updates on the main LVFS remote.
* A *lot* of machines using fwupd have never connected to the internet and we still want to enumerate hardware for audit and verification purposes.
* Re-enumerating all physical hardware (to get the latest quirks) when updating the metadata is not straightforward.
* The VID/PID is not necessarily the information that the plugin matches to assign the firmware stream, and so this would have to be a separate facet of metadata -- which may have to include quirks too.
Matching devices to plugins should be thought of as *orthogonal* to matching firmware to devices.
To simplify deployment it should be possible to allow the device itself to specify the plugin to use and optionally additional quirk data.
## Specification
Devices that implement a fwupd BOS DS20 descriptor can be updated without always needing to update the runtime version of fwupd for updated quirk entries, assuming the device is updatable by the existing vendor plugin.
USB devices can create a new platform device capability BOS *“Binary Object Store”* descriptor with `bDevCapabilityType=0x05` and a fwupd-specific UUID, specifically `010aec63-f574-52cd-9dda-2852550d94f0`.
This UUID is generated type-5 SHA-1 hash of the word `fwupd` using a DNS namespace.
Using this custom UUID will ensure that Microsoft Windows does not try to parse the capability descriptor as a *Microsoft OS 2.0 descriptor set*.
## Implementation
Create a BOS DS20 descriptor such as:
1C 10 05 00 63 ec 0a 01 74 f5 cd 52 9d da 28 52 55 0d 94 f0 05 08 01 00 20 00 2a 00
├┘ ├┘ ├┘ ├┘ └─────────────┬───────────────────────────────┘ └─┬───────┘ └─┬─┘ ├┘ ├┘
│ │ │ │ └─PlatformCapabilityUUID │ wLength─┘ │ │
│ │ │ └─bReserved dwVersion─┘ bVendorCode─┘ │
│ │ └────bDevCapability bAltEnumCmd────┘
│ └───────bDescriptorType
└──────────bLength
The `dwVersion` encoded here is fwupd `1.8.5`, which is also the first version that supports BOS DS20 descriptors.
The BOS descriptors are sorted by the requester and can appear in any order.
The descriptor with the highest `dwVersion` that is not newer than the current running daemon version is used.
This means that `dwVersion` is effectively the minimum version of fwupd that should read the descriptor.
This allows devices to set different quirks depending on the fwupd version, although in practice this should not be required.
A suitable bVendorCode should be chosen that is not used for existing device operation.
* The `dwVersion` parameter **must** be larger than `0x00010805`, i.e. 1.8.5.
* The `bAltEnumCmd` parameter **must** be zero.
* The `PlatformCapabilityUUID` **must** be `010aec63-f574-52cd-9dda-2852550d94f0`.
Then allow the device to reply to a USB control request with the following parameters:
transfer-direction: device-to-host
request-type: vendor
recipient: device
bRequest: {value of bVendorCode in the BOS descriptor}
wValue: 0x00
wIndex: 0x07
wLength: {value of wLength in the BOS descriptor}
The device should return something like:
50 6c 75 67 69 6e 3d 64 66 75 0a 49 63 6f 6e 3d 63 6f 6d 70 75 74 65 72 0a 00 00 00 00 00 00 00
...which is the UTF-8 quirk data, e.g.
Plugin=dfu
Icon=computer
The UTF-8 quirk data must **not** contain Windows *CRLF-style* line endings.
## Workflow
To generate the fwupd DS20 descriptor save a file such as `fw-ds20.builder.xml`:
<firmware gtype="FuUsbDeviceFwDs20">
<idx>42</idx> <!-- bVendorCode -->
<size>32</size> <!-- wLength -->
</firmware>
Then run `fwupdtool firmware-build fw-ds20.builder.xml fw-ds20.bin`.
To generate the control transfer response, save a file such as `fw-ds20.quirk`:
[USB\VID_273F&PID_1004]
Plugin = dfu
Icon = computer
Then run `contrib/generate-ds20.py fw-ds20.quirk --bufsz 32`.
The maximum buffer size is typically hardcoded for the device and may be specified in a microcontroller datasheet.
## Prior Art
### Hardcoded Class & Subclass
The fwupd project already support two plugins that use the USB class code, rather than the exact instance ID with the VID/PID.
For example, this DFU entry means *“match any USB device with class 0xFE (application specific) and subclass 0x01”*:
[USB\CLASS_FE&SUBCLASS_01]
Plugin = dfu
These kind of devices do not need any device to plugin mapping (although, they still might need a quirk if they are non-complaint in some way, for example needing `Flags = detach-for-attach`) but in the most cases they just work.
The same can be done for Fastboot devices, matching class `0xFF` (vendor specific), subclass `0x42` and protocol `0x03`, although there is the same caveat for non-compliant devices that need quirk entries like `FastbootOperationDelay = 250`:
[USB\CLASS_FF&SUBCLASS_42&PROT_03]
Plugin = fastboot
#### Microsoft OS Descriptors 1.0
The [Microsoft OS Descriptors 1.0 Specification](https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-os-1-0-descriptors-specification) defines a device-specific variable-length metadata block of `CompatibleID`:`SubCompatibleID` on a string index of `0xEE` where the `CompatibleID` and `SubCompatibleID` are also both hardcoded at 8 bytes.
Using `FWUPDPLU` or `FWUPDFLA` as the `CompatibleID` would be acceptable, but we could not fit the plugin name (e.g. `logitech-bulkcontroller`) or the GUID (16 bytes) in an 8 byte `SubCompatibleID`.
Some non-compliant devices also hang and stop working when probing this specific string index.
#### Microsoft OS Descriptors 2.0
The [Microsoft OS Descriptors 2.0 Specification](https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-os-2-0-descriptors-specification) is more useful as it defines a new device capability that can return a variable length vendor-defined section of data, using a UUID as a key.
Using the `BOS` *“Binary Object Store”* descriptor is only available for devices using USB specification version 2.1 and newer.
The BOS descriptor is used for Wireless USB details, USB 2.0 extensions, SuperSpeed USB connection details and a Container ID.
The BOS descriptor does give the OS the ability to parse the platform capability, which is `bDevCapabilityType=0x05`. For UUID `D8DD60DF-4589-4CC7-9CD2-659D9E648A9F` this is identified as a structured blob of data Microsoft Windows uses for the suspend mode of the device.
Creating a new `bDevCapabilityType` would allow vendors to store a binary blob (e.g. `Plugin=foobarbaz\nFlags=QuirkValueHere\n`) but that would be out-of-specification and difficult to implement.

View File

@ -45,6 +45,7 @@ content_files = [
"env.md",
"tutorial.md",
"hsi.md",
"ds20.md",
"bios-settings.md",
]
content_images = [

View File

@ -66,6 +66,15 @@
<ul>
<li><a href="libfwupdplugin/bios-settings.html">User documentation</a></li>
</ul>
<header>
<h1>BOS DS20 Specification</h1>
<div class="description">
<p>The fwupd Binary Object Store descriptor specification</p>
</div>
</header>
<ul>
<li><a href="libfwupdplugin/ds20.html">Documentation</a></li>
</ul>
</section>
</body>
</html>

View File

@ -0,0 +1,22 @@
/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#pragma once
#include "fu-backend.h"
gboolean
fu_backend_load(FuBackend *self,
JsonObject *json_object,
const gchar *tag,
FuBackendLoadFlags flags,
GError **error);
gboolean
fu_backend_save(FuBackend *self,
JsonBuilder *json_builder,
const gchar *tag,
FuBackendSaveFlags flags,
GError **error);

View File

@ -8,7 +8,7 @@
#include "config.h"
#include "fu-backend.h"
#include "fu-backend-private.h"
#include "fu-string.h"
/**
@ -220,6 +220,74 @@ fu_backend_setup(FuBackend *self, FuProgress *progress, GError **error)
return TRUE;
}
/**
* fu_backend_load:
* @self: a #FuBackend
* @json_object: a #JsonObject
* @tag: a string backend tag, or %NULL
* @flags: %FuBackendLoadFlags, typically `FU_BACKEND_LOAD_FLAG_NONE`
* @error: (nullable): optional return location for an error
*
* Loads the backend from a JSON object.
*
* Returns: %TRUE for success
*
* Since: 1.8.5
**/
gboolean
fu_backend_load(FuBackend *self,
JsonObject *json_object,
const gchar *tag,
FuBackendLoadFlags flags,
GError **error)
{
FuBackendClass *klass = FU_BACKEND_GET_CLASS(self);
g_return_val_if_fail(FU_IS_BACKEND(self), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* optional */
if (klass->load != NULL) {
if (!klass->load(self, json_object, tag, flags, error))
return FALSE;
}
return TRUE;
}
/**
* fu_backend_save:
* @self: a #FuBackend
* @json_builder: a #JsonBuilder
* @tag: a string backend tag, or %NULL
* @flags: %FuBackendSaveFlags, typically `FU_BACKEND_SAVE_FLAG_NONE`
* @error: (nullable): optional return location for an error
*
* Saves the backend to a JSON builder.
*
* Returns: %TRUE for success
*
* Since: 1.8.5
**/
gboolean
fu_backend_save(FuBackend *self,
JsonBuilder *json_builder,
const gchar *tag,
FuBackendSaveFlags flags,
GError **error)
{
FuBackendClass *klass = FU_BACKEND_GET_CLASS(self);
g_return_val_if_fail(FU_IS_BACKEND(self), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* optional */
if (klass->save != NULL) {
if (!klass->save(self, json_builder, tag, flags, error))
return FALSE;
}
return TRUE;
}
/**
* fu_backend_coldplug:
* @self: a #FuBackend

View File

@ -6,12 +6,22 @@
#pragma once
#include <json-glib/json-glib.h>
#include "fu-context.h"
#include "fu-device.h"
#define FU_TYPE_BACKEND (fu_backend_get_type())
G_DECLARE_DERIVABLE_TYPE(FuBackend, fu_backend, FU, BACKEND, GObject)
typedef enum {
FU_BACKEND_LOAD_FLAG_NONE,
} FuBackendLoadFlags;
typedef enum {
FU_BACKEND_SAVE_FLAG_NONE,
} FuBackendSaveFlags;
struct _FuBackendClass {
GObjectClass parent_class;
/* signals */
@ -24,8 +34,18 @@ struct _FuBackendClass {
void (*registered)(FuBackend *self, FuDevice *device);
void (*invalidate)(FuBackend *self);
void (*to_string)(FuBackend *self, guint indent, GString *str);
gboolean (*load)(FuBackend *self,
JsonObject *json_object,
const gchar *tag,
FuBackendLoadFlags flags,
GError **error);
gboolean (*save)(FuBackend *self,
JsonBuilder *json_builder,
const gchar *tag,
FuBackendSaveFlags flags,
GError **error);
/*< private >*/
gpointer padding[26];
gpointer padding[24];
};
const gchar *

View File

@ -21,6 +21,7 @@
*/
typedef struct {
FuContextFlags flags;
FuHwids *hwids;
FuSmbios *smbios;
FuQuirks *quirks;
@ -830,6 +831,42 @@ fu_context_set_battery_threshold(FuContext *self, guint battery_threshold)
g_object_notify(G_OBJECT(self), "battery-threshold");
}
/**
* fu_context_add_flag:
* @context: a #FuContext
* @flag: the context flag
*
* Adds a specific flag to the context.
*
* Since: 1.8.5
**/
void
fu_context_add_flag(FuContext *context, FuContextFlags flag)
{
FuContextPrivate *priv = GET_PRIVATE(context);
g_return_if_fail(FU_IS_CONTEXT(context));
priv->flags |= flag;
}
/**
* fu_context_has_flag:
* @context: a #FuContext
* @flag: the context flag
*
* Finds if the context has a specific flag.
*
* Returns: %TRUE if the flag is set
*
* Since: 1.8.5
**/
gboolean
fu_context_has_flag(FuContext *context, FuContextFlags flag)
{
FuContextPrivate *priv = GET_PRIVATE(context);
g_return_val_if_fail(FU_IS_CONTEXT(context), FALSE);
return (priv->flags & flag) > 0;
}
static void
fu_context_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{

View File

@ -36,6 +36,36 @@ typedef void (*FuContextLookupIter)(FuContext *self,
const gchar *value,
gpointer user_data);
/**
* FU_CONTEXT_FLAG_NONE:
*
* No flags set.
*
* Since: 1.8.5
**/
#define FU_CONTEXT_FLAG_NONE (0u)
/**
* FU_CONTEXT_FLAG_SAVE_EVENTS:
*
* Save events so that they can be replayed to emulate devices.
*
* Since: 1.8.5
**/
#define FU_CONTEXT_FLAG_SAVE_EVENTS (1u << 0)
/**
* FuContextFlags:
*
* The context flags.
**/
typedef guint64 FuContextFlags;
void
fu_context_add_flag(FuContext *context, FuContextFlags flag);
gboolean
fu_context_has_flag(FuContext *context, FuContextFlags flag);
const gchar *
fu_context_get_smbios_string(FuContext *self, guint8 structure_type, guint8 offset);
guint

View File

@ -0,0 +1,317 @@
/*
* 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-dump.h"
#include "fu-mem.h"
#include "fu-usb-device-ds20.h"
/**
* FuUsbDeviceDs20:
*
* A USB DS20 BOS descriptor.
*
* See also: [class@FuUsbDevice]
*/
typedef struct {
guint32 version_lowest;
} FuUsbDeviceDs20Private;
G_DEFINE_TYPE_WITH_PRIVATE(FuUsbDeviceDs20, fu_usb_device_ds20, FU_TYPE_FIRMWARE)
#define GET_PRIVATE(o) (fu_usb_device_ds20_get_instance_private(o))
typedef struct __attribute__((packed)) {
guint32 platform_ver;
guint16 total_length;
guint8 vendor_code;
guint8 alt_code;
} FuUsbDeviceDs20Item;
/**
* fu_usb_device_ds20_set_version_lowest:
* @self: a #FuUsbDeviceDs20
* @version_lowest: version number
*
* Sets the lowest possible `platform_ver` for a DS20 descriptor.
*
* Since: 1.8.5
**/
void
fu_usb_device_ds20_set_version_lowest(FuUsbDeviceDs20 *self, guint32 version_lowest)
{
FuUsbDeviceDs20Private *priv = GET_PRIVATE(self);
g_return_if_fail(FU_IS_USB_DEVICE_DS20(self));
priv->version_lowest = version_lowest;
}
/**
* fu_usb_device_ds20_apply_to_device:
* @self: a #FuUsbDeviceDs20
* @device: a #FuUsbDevice
* @error: (nullable): optional return location for an error
*
* Sets the DS20 descriptor onto @device.
*
* Returns: %TRUE for success
*
* Since: 1.8.5
**/
gboolean
fu_usb_device_ds20_apply_to_device(FuUsbDeviceDs20 *self, FuUsbDevice *device, GError **error)
{
#ifdef HAVE_GUSB
FuUsbDeviceDs20Class *klass = FU_USB_DEVICE_DS20_GET_CLASS(self);
GUsbDevice *usb_device = fu_usb_device_get_dev(device);
gsize actual_length = 0;
gsize total_length = fu_firmware_get_size(FU_FIRMWARE(self));
guint8 vendor_code = fu_firmware_get_idx(FU_FIRMWARE(self));
g_autofree guint8 *buf = g_malloc0(total_length);
g_autoptr(GBytes) blob = NULL;
g_return_val_if_fail(FU_IS_USB_DEVICE_DS20(self), FALSE);
g_return_val_if_fail(FU_IS_USB_DEVICE(device), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (!g_usb_device_control_transfer(usb_device,
G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
G_USB_DEVICE_REQUEST_TYPE_VENDOR,
G_USB_DEVICE_RECIPIENT_DEVICE,
vendor_code, /* bRequest */
0x0, /* wValue */
0x07, /* wIndex */
buf,
total_length,
&actual_length,
500,
NULL, /* cancellable */
error)) {
g_prefix_error(error, "requested vendor code 0x%02x: ", vendor_code);
return FALSE;
}
if (total_length != actual_length) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"expected 0x%x bytes from vendor code 0x%02x, but got 0x%x",
(guint)total_length,
vendor_code,
(guint)actual_length);
return FALSE;
}
/* debug */
if (g_getenv("FWUPD_VERBOSE") != NULL)
fu_dump_raw(G_LOG_DOMAIN, "PlatformCapabilityOs20", buf, actual_length);
/* FuUsbDeviceDs20->parse */
blob = g_bytes_new_take(g_steal_pointer(&buf), actual_length);
return klass->parse(self, blob, device, error);
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"GUsb support is unavailable");
return FALSE;
#endif
}
static gboolean
fu_usb_device_ds20_check_magic(FuFirmware *firmware, GBytes *fw, gsize offset, GError **error)
{
fwupd_guid_t guid = {0x0};
g_autofree gchar *guid_str = NULL;
/* parse GUID */
if (!fu_memcpy_safe((guint8 *)&guid,
sizeof(guid),
0x0, /* dst */
g_bytes_get_data(fw, NULL),
g_bytes_get_size(fw),
offset + 0x1, /* src */
sizeof(guid),
error))
return FALSE;
/* matches the correct UUID */
guid_str = fwupd_guid_to_string(&guid, FWUPD_GUID_FLAG_MIXED_ENDIAN);
if (g_strcmp0(guid_str, fu_firmware_get_id(firmware)) != 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid UUID for DS20, expected %s",
fu_firmware_get_id(firmware));
return FALSE;
}
/* success */
return TRUE;
}
static gint
fu_usb_device_ds20_sort_by_platform_ver_cb(gconstpointer a, gconstpointer b)
{
FuUsbDeviceDs20Item *ds1 = *((FuUsbDeviceDs20Item **)a);
FuUsbDeviceDs20Item *ds2 = *((FuUsbDeviceDs20Item **)b);
if (ds1->platform_ver < ds2->platform_ver)
return -1;
if (ds1->platform_ver > ds2->platform_ver)
return 1;
return 0;
}
static gboolean
fu_usb_device_ds20_parse(FuFirmware *firmware,
GBytes *fw,
gsize offset,
FwupdInstallFlags flags,
GError **error)
{
FuUsbDeviceDs20 *self = FU_USB_DEVICE_DS20(firmware);
FuUsbDeviceDs20Private *priv = GET_PRIVATE(self);
const guint8 *buf;
gsize bufsz = 0;
guint version_lowest = fu_firmware_get_version_raw(firmware);
g_autoptr(GBytes) blob = NULL;
g_autoptr(GPtrArray) dsinfos = g_ptr_array_new_with_free_func(g_free);
/* cut out the data */
blob = fu_bytes_new_offset(fw,
1 + sizeof(fwupd_guid_t),
g_bytes_get_size(fw) - (1 + sizeof(fwupd_guid_t)),
error);
if (blob == NULL)
return FALSE;
buf = g_bytes_get_data(blob, &bufsz);
for (gsize off = 0; off < bufsz; off += sizeof(FuUsbDeviceDs20Item)) {
FuUsbDeviceDs20Item *dsinfo = g_new0(FuUsbDeviceDs20Item, 1);
guint16 total_length = 0;
guint32 platform_ver = 0;
g_ptr_array_add(dsinfos, dsinfo);
if (!fu_memread_uint32_safe(buf,
bufsz,
off +
G_STRUCT_OFFSET(FuUsbDeviceDs20Item, platform_ver),
&platform_ver,
G_LITTLE_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint16_safe(buf,
bufsz,
off +
G_STRUCT_OFFSET(FuUsbDeviceDs20Item, total_length),
&total_length,
G_LITTLE_ENDIAN,
error))
return FALSE;
if (!fu_memread_uint8_safe(buf,
bufsz,
off + G_STRUCT_OFFSET(FuUsbDeviceDs20Item, vendor_code),
&dsinfo->vendor_code,
error))
return FALSE;
if (!fu_memread_uint8_safe(buf,
bufsz,
off + G_STRUCT_OFFSET(FuUsbDeviceDs20Item, alt_code),
&dsinfo->alt_code,
error))
return FALSE;
dsinfo->platform_ver = platform_ver;
dsinfo->total_length = total_length;
g_debug("PlatformVersion=0x%08x, TotalLength=0x%04x, VendorCode=0x%02x, "
"AltCode=0x%02x",
dsinfo->platform_ver,
dsinfo->total_length,
dsinfo->vendor_code,
dsinfo->alt_code);
}
/* sort by platform_ver, highest first */
g_ptr_array_sort(dsinfos, fu_usb_device_ds20_sort_by_platform_ver_cb);
/* find the newest info that's not newer than the lowest version */
for (guint i = 0; i < dsinfos->len; i++) {
FuUsbDeviceDs20Item *dsinfo = g_ptr_array_index(dsinfos, i);
/* not valid */
if (dsinfo->platform_ver == 0x0) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"invalid platform version 0x%08x",
dsinfo->platform_ver);
return FALSE;
}
if (dsinfo->platform_ver < priv->version_lowest) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"invalid platform version 0x%08x, expected >= 0x%08x",
dsinfo->platform_ver,
priv->version_lowest);
return FALSE;
}
/* dwVersion is effectively the minimum version */
if (dsinfo->platform_ver <= version_lowest) {
fu_firmware_set_size(firmware, dsinfo->total_length);
fu_firmware_set_idx(firmware, dsinfo->vendor_code);
return TRUE;
}
}
/* failed */
g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "no supported platform version");
return FALSE;
}
static GBytes *
fu_usb_device_ds20_write(FuFirmware *firmware, GError **error)
{
fwupd_guid_t guid = {0x0};
g_autoptr(GByteArray) buf = g_byte_array_new();
/* bReserved */
fu_byte_array_append_uint8(buf, 0x0);
/* PlatformCapabilityUUID */
if (!fwupd_guid_from_string(fu_firmware_get_id(firmware),
&guid,
FWUPD_GUID_FLAG_MIXED_ENDIAN,
error))
return NULL;
g_byte_array_append(buf, (const guint8 *)&guid, sizeof(guid));
/* CapabilityData */
fu_byte_array_append_uint32(buf, fu_firmware_get_version_raw(firmware), G_LITTLE_ENDIAN);
fu_byte_array_append_uint16(buf, fu_firmware_get_size(firmware), G_LITTLE_ENDIAN);
fu_byte_array_append_uint8(buf, fu_firmware_get_idx(firmware));
fu_byte_array_append_uint8(buf, 0x0); /* AltCode */
/* success */
return g_byte_array_free_to_bytes(g_steal_pointer(&buf));
}
static void
fu_usb_device_ds20_init(FuUsbDeviceDs20 *self)
{
fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE);
}
static void
fu_usb_device_ds20_class_init(FuUsbDeviceDs20Class *klass)
{
FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
klass_firmware->check_magic = fu_usb_device_ds20_check_magic;
klass_firmware->parse = fu_usb_device_ds20_parse;
klass_firmware->write = fu_usb_device_ds20_write;
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#pragma once
#include "fu-firmware.h"
#include "fu-usb-device.h"
#define FU_TYPE_USB_DEVICE_DS20 (fu_usb_device_ds20_get_type())
G_DECLARE_DERIVABLE_TYPE(FuUsbDeviceDs20, fu_usb_device_ds20, FU, USB_DEVICE_DS20, FuFirmware)
struct _FuUsbDeviceDs20Class {
FuFirmwareClass parent_class;
gboolean (*parse)(FuUsbDeviceDs20 *self, GBytes *blob, FuUsbDevice *device, GError **error)
G_GNUC_WARN_UNUSED_RESULT;
/*< private >*/
gpointer padding[30];
};
void
fu_usb_device_ds20_set_version_lowest(FuUsbDeviceDs20 *self, guint32 version_lowest);
gboolean
fu_usb_device_ds20_apply_to_device(FuUsbDeviceDs20 *self, FuUsbDevice *device, GError **error);

View File

@ -0,0 +1,113 @@
/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include "fu-device-private.h"
#include "fu-string.h"
#include "fu-usb-device-fw-ds20.h"
#include "fu-version.h"
struct _FuUsbDeviceFwDs20 {
FuUsbDeviceDs20 parent_instance;
};
G_DEFINE_TYPE(FuUsbDeviceFwDs20, fu_usb_device_fw_ds20, FU_TYPE_USB_DEVICE_DS20)
#define DS20_VERSION_LOWEST ((1u << 16) | (8u << 8) | 5u)
#define DS20_VERSION_CURRENT \
((((guint32)FU_MAJOR_VERSION) << 16) | (((guint32)FU_MINOR_VERSION) << 8) | \
((guint)FU_MICRO_VERSION))
static gboolean
fu_usb_device_fw_ds20_parse(FuUsbDeviceDs20 *self,
GBytes *blob,
FuUsbDevice *device,
GError **error)
{
gsize bufsz = 0;
gsize bufsz_safe;
const guint8 *buf = g_bytes_get_data(blob, &bufsz);
g_auto(GStrv) lines = NULL;
/* only accept Linux line-endings */
if (g_strstr_len((const gchar *)buf, bufsz, "\r") != NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Windows line endings are not supported");
return FALSE;
}
/* find the first NUL, if one exists */
for (bufsz_safe = 0; buf[bufsz_safe] != '\0' && bufsz_safe < bufsz; bufsz_safe++)
;
if (!g_utf8_validate((const gchar *)buf, (gssize)bufsz_safe, NULL)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"DS20 descriptor is not valid UTF-8");
return FALSE;
}
/* add payload for ->export() */
fu_firmware_set_bytes(FU_FIRMWARE(self), blob);
/* split into lines */
lines = fu_strsplit((const gchar *)buf, bufsz_safe, "\n", -1);
for (guint i = 0; lines[i] != NULL; i++) {
g_auto(GStrv) kv = NULL;
if (lines[i][0] == '\0')
continue;
kv = g_strsplit(lines[i], "=", 2);
if (g_strv_length(kv) < 2) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"expected key=value for '%s'",
lines[i]);
return FALSE;
}
/* it's fine to be strict here, as we checked the fwupd version was new enough in
* FuUsbDeviceDs20Item */
g_debug("setting ds20 device quirk '%s'='%s'", kv[0], kv[1]);
if (!fu_device_set_quirk_kv(FU_DEVICE(device), kv[0], kv[1], error))
return FALSE;
}
/* success */
return TRUE;
}
static void
fu_usb_device_fw_ds20_class_init(FuUsbDeviceFwDs20Class *klass)
{
FuUsbDeviceDs20Class *usb_device_ds20_klass = FU_USB_DEVICE_DS20_CLASS(klass);
usb_device_ds20_klass->parse = fu_usb_device_fw_ds20_parse;
}
static void
fu_usb_device_fw_ds20_init(FuUsbDeviceFwDs20 *self)
{
fu_firmware_set_version_raw(FU_FIRMWARE(self), DS20_VERSION_CURRENT);
fu_usb_device_ds20_set_version_lowest(FU_USB_DEVICE_DS20(self), DS20_VERSION_LOWEST);
fu_firmware_set_id(FU_FIRMWARE(self), "010aec63-f574-52cd-9dda-2852550d94f0");
}
/**
* fu_usb_device_fw_ds20_new:
*
* Creates a new #FuUsbDeviceFwDs20.
*
* Returns: (transfer full): a #FuFirmware
*
* Since: 1.8.5
**/
FuFirmware *
fu_usb_device_fw_ds20_new(void)
{
return g_object_new(FU_TYPE_USB_DEVICE_FW_DS20, NULL);
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#pragma once
#include "fu-usb-device-ds20.h"
#define FU_TYPE_USB_DEVICE_FW_DS20 (fu_usb_device_fw_ds20_get_type())
G_DECLARE_FINAL_TYPE(FuUsbDeviceFwDs20,
fu_usb_device_fw_ds20,
FU,
USB_DEVICE_FW_DS20,
FuUsbDeviceDs20)
FuFirmware *
fu_usb_device_fw_ds20_new(void);

View File

@ -0,0 +1,119 @@
/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include "fu-mem.h"
#include "fu-usb-device-ms-ds20.h"
struct _FuUsbDeviceMsDs20 {
FuUsbDeviceDs20 parent_instance;
};
G_DEFINE_TYPE(FuUsbDeviceMsDs20, fu_usb_device_ms_ds20, FU_TYPE_USB_DEVICE_DS20)
#define USB_OS_20_SET_HEADER_DESCRIPTOR 0x00
#define USB_OS_20_SUBSET_HEADER_CONFIGURATION 0x01
#define USB_OS_20_SUBSET_HEADER_FUNCTION 0x02
#define USB_OS_20_FEATURE_COMPATBLE_ID 0x03
#define USB_OS_20_FEATURE_REG_PROPERTY 0x04
#define USB_OS_20_FEATURE_MIN_RESUME_TIME 0x05
#define USB_OS_20_FEATURE_MODEL_ID 0x06
#define USB_OS_20_FEATURE_CCGP_DEVICE 0x07
#define USB_OS_20_FEATURE_VENDOR_REVISION 0x08
static const gchar *
fu_usb_device_os20_type_to_string(guint16 type)
{
if (type == USB_OS_20_SET_HEADER_DESCRIPTOR)
return "set-header-descriptor";
if (type == USB_OS_20_SUBSET_HEADER_CONFIGURATION)
return "subset-header-configuration";
if (type == USB_OS_20_SUBSET_HEADER_FUNCTION)
return "subset-header-function";
if (type == USB_OS_20_FEATURE_COMPATBLE_ID)
return "feature-compatble-id";
if (type == USB_OS_20_FEATURE_REG_PROPERTY)
return "feature-reg-property";
if (type == USB_OS_20_FEATURE_MIN_RESUME_TIME)
return "feature-min-resume-time";
if (type == USB_OS_20_FEATURE_MODEL_ID)
return "feature-model-id";
if (type == USB_OS_20_FEATURE_CCGP_DEVICE)
return "feature-ccgp-device";
if (type == USB_OS_20_FEATURE_VENDOR_REVISION)
return "feature-vendor-revision";
return NULL;
}
static gboolean
fu_usb_device_ms_ds20_parse(FuUsbDeviceDs20 *self,
GBytes *blob,
FuUsbDevice *device,
GError **error)
{
gsize bufsz = 0;
const guint8 *buf = g_bytes_get_data(blob, &bufsz);
/* get length and type only */
for (gsize offset = 0; offset < bufsz;) {
guint16 desc_sz = 0;
guint16 desc_type = 0;
if (!fu_memread_uint16_safe(buf,
bufsz,
offset + 0x0,
&desc_sz,
G_LITTLE_ENDIAN,
error))
return FALSE;
if (desc_sz == 0)
break;
if (!fu_memread_uint16_safe(buf,
bufsz,
offset + 0x2,
&desc_type,
G_LITTLE_ENDIAN,
error))
return FALSE;
g_debug("USB OS descriptor type 0x%04x [%s], length 0x%04x",
desc_type,
fu_usb_device_os20_type_to_string(desc_type),
desc_sz);
offset += desc_sz;
}
/* success */
return TRUE;
}
static void
fu_usb_device_ms_ds20_class_init(FuUsbDeviceMsDs20Class *klass)
{
FuUsbDeviceDs20Class *usb_device_ds20_klass = FU_USB_DEVICE_DS20_CLASS(klass);
usb_device_ds20_klass->parse = fu_usb_device_ms_ds20_parse;
}
static void
fu_usb_device_ms_ds20_init(FuUsbDeviceMsDs20 *self)
{
fu_firmware_set_version_raw(FU_FIRMWARE(self), 0x06030000); /* Windows 8.1 */
fu_firmware_set_id(FU_FIRMWARE(self), "d8dd60df-4589-4cc7-9cd2-659d9e648a9f");
}
/**
* fu_usb_device_ms_ds20_new:
*
* Creates a new #FuUsbDeviceMsDs20.
*
* Returns: (transfer full): a #FuFirmware
*
* Since: 1.8.5
**/
FuFirmware *
fu_usb_device_ms_ds20_new(void)
{
return g_object_new(FU_TYPE_USB_DEVICE_MS_DS20, NULL);
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#pragma once
#include "fu-usb-device-ds20.h"
#define FU_TYPE_USB_DEVICE_MS_DS20 (fu_usb_device_ms_ds20_get_type())
G_DECLARE_FINAL_TYPE(FuUsbDeviceMsDs20,
fu_usb_device_ms_ds20,
FU,
USB_DEVICE_MS_DS20,
FuUsbDeviceDs20)
FuFirmware *
fu_usb_device_ms_ds20_new(void);

View File

@ -12,6 +12,8 @@
#include "fu-dump.h"
#include "fu-mem.h"
#include "fu-string.h"
#include "fu-usb-device-fw-ds20.h"
#include "fu-usb-device-ms-ds20.h"
#include "fu-usb-device-private.h"
/**
@ -274,6 +276,9 @@ fu_usb_device_setup(FuDevice *device, GError **error)
FuUsbDevice *self = FU_USB_DEVICE(device);
FuUsbDevicePrivate *priv = GET_PRIVATE(self);
guint idx;
#if G_USB_CHECK_VERSION(0, 4, 0)
g_autoptr(GPtrArray) bos_descriptors = NULL;
#endif
g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
@ -340,6 +345,41 @@ fu_usb_device_setup(FuDevice *device, GError **error)
if (!fu_usb_device_query_hub(self, error))
return FALSE;
}
#if G_USB_CHECK_VERSION(0, 4, 0)
/* get the platform capability BOS descriptors */
bos_descriptors = g_usb_device_get_bos_descriptors(priv->usb_device, NULL);
for (guint i = 0; bos_descriptors != NULL && i < bos_descriptors->len; i++) {
GUsbBosDescriptor *bos = g_ptr_array_index(bos_descriptors, i);
GBytes *extra = g_usb_bos_descriptor_get_extra(bos);
if (g_usb_bos_descriptor_get_capability(bos) == 0x5 &&
g_bytes_get_size(extra) > 0) {
g_autoptr(FuFirmware) ds20 = NULL;
g_autoptr(GError) error_ds20 = NULL;
g_autofree gchar *str = NULL;
ds20 = fu_firmware_new_from_gtypes(extra,
FWUPD_INSTALL_FLAG_NONE,
&error_ds20,
FU_TYPE_USB_DEVICE_FW_DS20,
FU_TYPE_USB_DEVICE_MS_DS20,
G_TYPE_INVALID);
if (ds20 == NULL) {
g_warning("failed to parse platform capability BOS descriptor: %s",
error_ds20->message);
continue;
}
if (!fu_usb_device_ds20_apply_to_device(FU_USB_DEVICE_DS20(ds20),
self,
&error_ds20)) {
g_warning("failed to get DS20 data: %s", error_ds20->message);
continue;
}
str = fu_firmware_to_string(ds20);
g_debug("DS20: %s", str);
}
}
#endif
#endif
/* success */

View File

@ -1098,6 +1098,10 @@ LIBFWUPDPLUGIN_1.8.4 {
LIBFWUPDPLUGIN_1.8.5 {
global:
fu_backend_load;
fu_backend_save;
fu_context_add_flag;
fu_context_has_flag;
fu_device_set_quirk_kv;
fu_intel_thunderbolt_firmware_get_type;
fu_intel_thunderbolt_firmware_new;
@ -1111,5 +1115,12 @@ LIBFWUPDPLUGIN_1.8.5 {
fu_intel_thunderbolt_nvm_is_native;
fu_intel_thunderbolt_nvm_new;
fu_kernel_get_cmdline;
fu_usb_device_ds20_apply_to_device;
fu_usb_device_ds20_get_type;
fu_usb_device_ds20_set_version_lowest;
fu_usb_device_fw_ds20_get_type;
fu_usb_device_fw_ds20_new;
fu_usb_device_ms_ds20_get_type;
fu_usb_device_ms_ds20_new;
local: *;
} LIBFWUPDPLUGIN_1.8.4;

View File

@ -78,6 +78,9 @@ fwupdplugin_src = [
'fu-i2c-device.c',
'fu-mei-device.c',
'fu-usb-device.c',
'fu-usb-device-ds20.c', # fuzzing
'fu-usb-device-fw-ds20.c', # fuzzing
'fu-usb-device-ms-ds20.c', # fuzzing
'fu-cfi-device.c',
'fu-hid-device.c',
'fu-linear-firmware.c',
@ -193,6 +196,7 @@ fu_hash = custom_target(
fwupdplugin_headers_private = [
fu_hash,
'fu-backend-private.h',
'fu-context-private.h',
'fu-device-private.h',
'fu-kenv.h',
@ -201,6 +205,9 @@ fwupdplugin_headers_private = [
'fu-security-attrs-private.h',
'fu-smbios-private.h',
'fu-udev-device-private.h',
'fu-usb-device-ds20.h',
'fu-usb-device-fw-ds20.h',
'fu-usb-device-ms-ds20.h',
'fu-usb-device-private.h',
fwupdplugin_version_h,
]

View File

@ -37,6 +37,7 @@
#include "fwupd-resources.h"
#include "fwupd-security-attr-private.h"
#include "fu-backend-private.h"
#include "fu-bios-settings-private.h"
#include "fu-cabinet.h"
#include "fu-context-private.h"
@ -60,6 +61,8 @@
#include "fu-security-attr-common.h"
#include "fu-security-attrs-private.h"
#include "fu-udev-device-private.h"
#include "fu-usb-device-fw-ds20.h"
#include "fu-usb-device-ms-ds20.h"
#include "fu-version.h"
#ifdef HAVE_GUDEV
@ -7760,6 +7763,8 @@ fu_engine_load(FuEngine *self, FuEngineLoadFlags flags, FuProgress *progress, GE
fu_context_add_firmware_gtype(self->ctx,
"intel-thunderbolt-nvm",
FU_TYPE_INTEL_THUNDERBOLT_NVM);
fu_context_add_firmware_gtype(self->ctx, "usb-device-fw-ds20", FU_TYPE_USB_DEVICE_FW_DS20);
fu_context_add_firmware_gtype(self->ctx, "usb-device-ms-ds20", FU_TYPE_USB_DEVICE_MS_DS20);
/* we are emulating a different host */
if (host_emulate != NULL) {
@ -8033,6 +8038,22 @@ fu_engine_idle_status_notify_cb(FuIdle *idle, GParamSpec *pspec, FuEngine *self)
fu_engine_set_status(self, status);
}
gboolean
fu_engine_backends_save(FuEngine *self, JsonBuilder *json_builder, GError **error)
{
json_builder_begin_object(json_builder);
json_builder_set_member_name(json_builder, "Backends");
json_builder_begin_array(json_builder);
for (guint i = 0; i < self->backends->len; i++) {
FuBackend *backend = g_ptr_array_index(self->backends, i);
if (!fu_backend_save(backend, json_builder, NULL, FU_BACKEND_SAVE_FLAG_NONE, error))
return FALSE;
}
json_builder_end_array(json_builder);
json_builder_end_object(json_builder);
return TRUE;
}
static void
fu_engine_init(FuEngine *self)
{

View File

@ -248,3 +248,5 @@ fu_engine_modify_bios_settings(FuEngine *self,
GHashTable *settings,
gboolean force_ro,
GError **error);
gboolean
fu_engine_backends_save(FuEngine *self, JsonBuilder *json_builder, GError **error);

View File

@ -16,6 +16,7 @@
#include "fwupd-bios-setting-private.h"
#include "fwupd-security-attr-private.h"
#include "fu-backend-private.h"
#include "fu-bios-settings-private.h"
#include "fu-cabinet-common.h"
#include "fu-config.h"
@ -32,6 +33,7 @@
#include "fu-security-attr-common.h"
#include "fu-smbios-private.h"
#include "fu-spawn.h"
#include "fu-usb-backend.h"
typedef struct {
FuPlugin *plugin;
@ -3176,6 +3178,157 @@ _plugin_device_register_cb(FuPlugin *plugin, FuDevice *device, gpointer user_dat
fu_plugin_runner_device_register(plugin, device);
}
/*
* To generate the fwupd DS20 descriptor in the usb-devices.json file save fw-ds20.builder.xml:
*
* <firmware gtype="FuUsbDeviceFwDs20">
* <idx>42</idx> <!-- bVendorCode -->
* <size>32</size> <!-- wLength -->
* </firmware>
*
* Then run:
*
* fwupdtool firmware-build fw-ds20.builder.xml fw-ds20.bin
* base64 fw-ds20.bin
*
* To generate the fake control transfer response, save fw-ds20.quirk:
*
* [USB\VID_273F&PID_1004]
* Plugin = dfu
* Icon = computer
*
* Then run:
*
* contrib/generate-ds20.py fw-ds20.quirk --bufsz 32
*/
static void
fu_backend_usb_func(gconstpointer user_data)
{
FuTest *self = (FuTest *)user_data;
gboolean ret;
FuDevice *device_tmp;
g_autofree gchar *gusb_emulate_fn = NULL;
g_autofree gchar *devicestr = NULL;
g_autoptr(FuBackend) backend = fu_usb_backend_new(self->ctx);
g_autoptr(FuDeviceLocker) locker = NULL;
g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);
g_autoptr(GError) error = NULL;
g_autoptr(GPtrArray) devices = NULL;
g_autoptr(GPtrArray) possible_plugins = NULL;
g_autoptr(JsonParser) parser = json_parser_new();
#if !G_USB_CHECK_VERSION(0, 4, 0)
g_test_skip("GUsb version too old");
return;
#endif
/* load the JSON into the backend */
gusb_emulate_fn = g_test_build_filename(G_TEST_DIST, "tests", "usb-devices.json", NULL);
ret = json_parser_load_from_file(parser, gusb_emulate_fn, &error);
g_assert_no_error(error);
g_assert_true(ret);
g_assert_cmpstr(fu_backend_get_name(backend), ==, "usb");
g_assert_true(fu_backend_get_enabled(backend));
ret = fu_backend_setup(backend, progress, &error);
g_assert_no_error(error);
g_assert_true(ret);
ret = fu_backend_load(backend,
json_node_get_object(json_parser_get_root(parser)),
NULL,
FU_BACKEND_LOAD_FLAG_NONE,
&error);
g_assert_no_error(error);
g_assert_true(ret);
ret = fu_backend_coldplug(backend, progress, &error);
g_assert_no_error(error);
g_assert_true(ret);
devices = fu_backend_get_devices(backend);
g_assert_cmpint(devices->len, ==, 1);
device_tmp = g_ptr_array_index(devices, 0);
fu_device_set_context(device_tmp, self->ctx);
locker = fu_device_locker_new(device_tmp, &error);
g_assert_no_error(error);
g_assert_nonnull(locker);
/* for debugging */
devicestr = fu_device_to_string(device_tmp);
g_debug("%s", devicestr);
/* check the device was processed correctly by FuUsbDevice */
g_assert_cmpstr(fu_device_get_name(device_tmp), ==, "ColorHug2");
g_assert_true(fu_device_has_instance_id(device_tmp, "USB\\VID_273F&PID_1004&REV_0002"));
g_assert_true(fu_device_has_vendor_id(device_tmp, "USB:0x273F"));
/* check the fwupd DS20 descriptor was parsed */
g_assert_true(fu_device_has_icon(device_tmp, "computer"));
possible_plugins = fu_device_get_possible_plugins(device_tmp);
g_assert_cmpint(possible_plugins->len, ==, 1);
g_assert_cmpstr(g_ptr_array_index(possible_plugins, 0), ==, "dfu");
}
static void
fu_backend_usb_invalid_func(gconstpointer user_data)
{
FuTest *self = (FuTest *)user_data;
gboolean ret;
FuDevice *device_tmp;
g_autofree gchar *gusb_emulate_fn = NULL;
g_autoptr(FuBackend) backend = fu_usb_backend_new(self->ctx);
g_autoptr(FuDeviceLocker) locker = NULL;
g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);
g_autoptr(GError) error = NULL;
g_autoptr(GPtrArray) devices = NULL;
g_autoptr(JsonParser) parser = json_parser_new();
#if !G_USB_CHECK_VERSION(0, 4, 0)
g_test_skip("GUsb version too old");
return;
#endif
/* load the JSON into the backend */
gusb_emulate_fn =
g_test_build_filename(G_TEST_DIST, "tests", "usb-devices-invalid.json", NULL);
ret = json_parser_load_from_file(parser, gusb_emulate_fn, &error);
g_assert_no_error(error);
g_assert_true(ret);
ret = fu_backend_setup(backend, progress, &error);
g_assert_no_error(error);
g_assert_true(ret);
ret = fu_backend_load(backend,
json_node_get_object(json_parser_get_root(parser)),
NULL,
FU_BACKEND_LOAD_FLAG_NONE,
&error);
g_assert_no_error(error);
g_assert_true(ret);
ret = fu_backend_coldplug(backend, progress, &error);
g_assert_no_error(error);
g_assert_true(ret);
devices = fu_backend_get_devices(backend);
g_assert_cmpint(devices->len, ==, 1);
device_tmp = g_ptr_array_index(devices, 0);
fu_device_set_context(device_tmp, self->ctx);
g_test_expect_message("FuUsbDevice",
G_LOG_LEVEL_WARNING,
"*invalid platform version 0x0000000a, expected >= 0x00010805*");
g_test_expect_message("FuUsbDevice",
G_LOG_LEVEL_WARNING,
"failed to parse * BOS descriptor: did not find magic*");
locker = fu_device_locker_new(device_tmp, &error);
g_assert_no_error(error);
g_assert_nonnull(locker);
/* check the device was processed correctly by FuUsbDevice */
g_assert_cmpstr(fu_device_get_name(device_tmp), ==, "ColorHug2");
g_assert_true(fu_device_has_instance_id(device_tmp, "USB\\VID_273F&PID_1004&REV_0002"));
g_assert_true(fu_device_has_vendor_id(device_tmp, "USB:0x273F"));
/* check the fwupd DS20 descriptor was *not* parsed */
g_assert_false(fu_device_has_icon(device_tmp, "computer"));
}
static void
fu_plugin_module_func(gconstpointer user_data)
{
@ -4689,6 +4842,8 @@ main(int argc, char **argv)
g_test_add_data_func("/fwupd/progressbar", self, fu_progressbar_func);
}
g_test_add_data_func("/fwupd/plugin{build-hash}", self, fu_plugin_hash_func);
g_test_add_data_func("/fwupd/backend{usb}", self, fu_backend_usb_func);
g_test_add_data_func("/fwupd/backend{usb-invalid}", self, fu_backend_usb_invalid_func);
g_test_add_data_func("/fwupd/plugin{module}", self, fu_plugin_module_func);
g_test_add_data_func("/fwupd/memcpy", self, fu_memcpy_func);
g_test_add_data_func("/fwupd/security-attr", self, fu_security_attr_func);

View File

@ -3368,6 +3368,34 @@ fu_util_setup_interactive(FuUtilPrivate *priv, GError **error)
return fu_util_setup_interactive_console(error);
}
static gboolean
fu_util_backends_save(FuUtilPrivate *priv, const gchar *fn, GError **error)
{
g_autofree gchar *data = NULL;
g_autoptr(JsonBuilder) json_builder = json_builder_new();
g_autoptr(JsonGenerator) json_generator = NULL;
g_autoptr(JsonNode) json_root = NULL;
/* export as a string */
if (!fu_engine_backends_save(priv->engine, json_builder, error))
return FALSE;
json_root = json_builder_get_root(json_builder);
json_generator = json_generator_new();
json_generator_set_pretty(json_generator, TRUE);
json_generator_set_root(json_generator, json_root);
data = json_generator_to_data(json_generator, NULL);
if (data == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Failed to convert to JSON string");
return FALSE;
}
/* save to file */
return g_file_set_contents(fn, data, -1, error);
}
int
main(int argc, char *argv[])
{
@ -3386,6 +3414,7 @@ main(int argc, char *argv[])
g_autoptr(GPtrArray) cmd_array = fu_util_cmd_array_new();
g_autofree gchar *cmd_descriptions = NULL;
g_autofree gchar *filter = NULL;
g_autofree gchar *save_backends_fn = NULL;
const GOptionEntry options[] = {
{"version",
'\0',
@ -3532,6 +3561,15 @@ main(int argc, char *argv[])
N_("Filter with a set of device flags using a ~ prefix to "
"exclude, e.g. 'internal,~needs-reboot'"),
NULL},
{"save-backends",
'\0',
0,
G_OPTION_ARG_STRING,
&save_backends_fn,
/* TRANSLATORS: command line option */
N_("Specify a filename to use to save backend events"),
/* TRANSLATORS: filename argument with path */
N_("FILENAME")},
{"json",
'\0',
0,
@ -3968,6 +4006,10 @@ main(int argc, char *argv[])
/* load engine */
priv->engine = fu_engine_new();
if (save_backends_fn != NULL) {
fu_context_add_flag(fu_engine_get_context(priv->engine),
FU_CONTEXT_FLAG_SAVE_EVENTS);
}
g_signal_connect(FU_ENGINE(priv->engine),
"device-request",
G_CALLBACK(fu_util_update_device_request_cb),
@ -4026,6 +4068,12 @@ main(int argc, char *argv[])
return EXIT_FAILURE;
}
/* dump devices */
if (save_backends_fn != NULL && !fu_util_backends_save(priv, save_backends_fn, &error)) {
g_printerr("%s\n", error->message);
return EXIT_FAILURE;
}
/* a good place to do the traceback */
if (fu_progress_get_profile(priv->progress)) {
g_autofree gchar *str = fu_progress_traceback(priv->progress);

View File

@ -120,6 +120,9 @@ static gboolean
fu_usb_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error)
{
FuUsbBackend *self = FU_USB_BACKEND(backend);
#if G_USB_CHECK_VERSION(0, 4, 0)
FuContext *ctx = fu_backend_get_context(backend);
#endif
g_autoptr(GPtrArray) usb_devices = NULL;
/* progress */
@ -127,6 +130,12 @@ fu_usb_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error
fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "enumerate");
fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 99, "add-devices");
#if G_USB_CHECK_VERSION(0, 4, 0)
/* save events */
if (fu_context_has_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS))
g_usb_context_set_flags(self->usb_ctx, G_USB_CONTEXT_FLAGS_SAVE_EVENTS);
#endif
/* no insight */
g_usb_context_enumerate(self->usb_ctx);
fu_progress_step_done(progress);
@ -148,6 +157,44 @@ fu_usb_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error
return TRUE;
}
static gboolean
fu_usb_backend_load(FuBackend *backend,
JsonObject *json_object,
const gchar *tag,
FuBackendLoadFlags flags,
GError **error)
{
#if G_USB_CHECK_VERSION(0, 4, 0)
FuUsbBackend *self = FU_USB_BACKEND(backend);
return g_usb_context_load(self->usb_ctx, json_object, error);
#else
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"GUsb version too old to load backends");
return FALSE;
#endif
}
static gboolean
fu_usb_backend_save(FuBackend *backend,
JsonBuilder *json_builder,
const gchar *tag,
FuBackendSaveFlags flags,
GError **error)
{
#if G_USB_CHECK_VERSION(0, 4, 0)
FuUsbBackend *self = FU_USB_BACKEND(backend);
return g_usb_context_save(self->usb_ctx, json_builder, error);
#else
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"GUsb version too old to save backends");
return FALSE;
#endif
}
static void
fu_usb_backend_registered(FuBackend *backend, FuDevice *device)
{
@ -170,6 +217,7 @@ fu_usb_backend_finalize(GObject *object)
FuUsbBackend *self = FU_USB_BACKEND(object);
if (self->usb_ctx != NULL) {
g_signal_handlers_disconnect_by_data(G_USB_CONTEXT(self->usb_ctx), self);
g_object_weak_unref(G_OBJECT(self->usb_ctx),
fu_usb_backend_context_finalized_cb,
self);
@ -191,6 +239,8 @@ fu_usb_backend_class_init(FuUsbBackendClass *klass)
object_class->finalize = fu_usb_backend_finalize;
klass_backend->setup = fu_usb_backend_setup;
klass_backend->coldplug = fu_usb_backend_coldplug;
klass_backend->load = fu_usb_backend_load;
klass_backend->save = fu_usb_backend_save;
klass_backend->registered = fu_usb_backend_registered;
}

View File

@ -0,0 +1,49 @@
{
"UsbDevices": [
{
"PlatformId": "usb:00",
"IdVendor": 10047,
"IdProduct": 4100,
"Device": 2,
"USB": 512,
"Manufacturer": 1,
"Product": 2,
"UsbBosDescriptors": [
{
"Comment": "version invalid",
"DevCapabilityType": 5,
"ExtraData": "AGPsCgF09c1SndooUlUNlPAKAAAAIAAqAA=="
},
{
"Comment": "UUID invalid",
"DevCapabilityType": 5,
"ExtraData": "AAAAAAAAAAAAAAAAAAAAAAAFCAEAIAAqAA=="
},
{
"Comment": "plugin invalid",
"DevCapabilityType": 5,
"ExtraData": "AGPsCgF09c1SndooUlUNlPAFCAEAIAArAA=="
}
],
"UsbEvents": [
{
"Id": "GetStringDescriptor:DescIndex=0x02",
"Data":
"Q29sb3JIdWcyAEcAAAAAAACwA4pgfwAAAN+Vneb9GHkAAAAAAAAAAEA42QAAAAAAwHVdKPx/AAAAAAAAAAAAAJiCXSj8fwAAR9bLiWB/AADAdV0o/H8AAOrE/IlgfwAAgHW/AAAAAAAQw9UAAAAAAJiCXSj8fwAAytnLiQEAAAA="
},
{
"Comment": "Plugin=dfu\nIcon=computer\n",
"Id":
"ControlTransfer:Direction=0x00,RequestType=0x02,Recipient=0x00,Request=0x2a,Value=0x0000,Idx=0x0007,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=,Length=0x20",
"Data": "UGx1Z2luPWRmdQpJY29uPWNvbXB1dGVyCgAAAAAAAAA="
},
{
"Comment": "Plugin=XXX",
"Id":
"ControlTransfer:Direction=0x00,RequestType=0x02,Recipient=0x00,Request=0x2b,Value=0x0000,Idx=0x0007,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=,Length=0x20",
"Data": "UGx1Z2luPVhYWAoAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
]
}
]
}

110
src/tests/usb-devices.json Normal file
View File

@ -0,0 +1,110 @@
{
"UsbDevices": [
{
"PlatformId": "usb:01:00:06",
"IdVendor": 10047,
"IdProduct": 4100,
"Device": 2,
"USB": 512,
"Manufacturer": 1,
"Product": 2,
"UsbBosDescriptors": [
{
"DevCapabilityType": 5,
"ExtraData": "AN9g3diJRcdMnNJlnZ5kip8AAAMG4AQVAA=="
},
{
"DevCapabilityType": 5,
"ExtraData": "AGPsCgF09c1SndooUlUNlPAFCAEAIAAqAA=="
},
{
"DevCapabilityType": 17,
"ExtraData": "AQMAAAA="
}
],
"UsbInterfaces": [
{
"Length": 9,
"DescriptorType": 4,
"InterfaceNumber": 1,
"InterfaceClass": 255,
"InterfaceSubClass": 70,
"InterfaceProtocol": 87,
"Interface": 3
},
{
"Length": 9,
"DescriptorType": 4,
"InterfaceNumber": 2,
"InterfaceClass": 255,
"InterfaceSubClass": 71,
"InterfaceProtocol": 85,
"Interface": 4
},
{
"Length": 9,
"DescriptorType": 4,
"InterfaceClass": 3,
"UsbEndpoints": [
{
"DescriptorType": 5,
"EndpointAddress": 129,
"Interval": 1,
"MaxPacketSize": 64
},
{
"DescriptorType": 5,
"EndpointAddress": 1,
"Interval": 1,
"MaxPacketSize": 64
}
],
"ExtraData": "CSERAQABIh0A"
}
],
"UsbEvents": [
{
"Id": "GetStringDescriptor:DescIndex=0x01",
"Data":
"SHVnaHNraSBMdGQuAAAAAAAAAAAAAAAAIFjfAAAAAAAAAAAAAAAAAEA42QAAAAAAwHVdKPx/AAAAAAAAAAAAAJiCXSj8fwAAR9bLiWB/AADAdV0o/H8AAOrE/IlgfwAAgHW/AAAAAAAQw9UAAAAAAJiCXSj8fwAAytnLiQEAAAA="
},
{
"Id": "GetStringDescriptor:DescIndex=0x02",
"Data":
"Q29sb3JIdWcyAEcAAAAAAACwA4pgfwAAAN+Vneb9GHkAAAAAAAAAAEA42QAAAAAAwHVdKPx/AAAAAAAAAAAAAJiCXSj8fwAAR9bLiWB/AADAdV0o/H8AAOrE/IlgfwAAgHW/AAAAAAAQw9UAAAAAAJiCXSj8fwAAytnLiQEAAAA="
},
{
"Id":
"GetCustomIndex:ClassId=0xff,SubclassId=0x46,ProtocolId=0x57",
"Data": "Aw=="
},
{
"Id": "GetStringDescriptor:DescIndex=0x03",
"Data":
"Mi4wLjcAAAAD0WmJYH8AAP8AAAAAAAAAA9FpiWB/AACQRNkAAAAAAGCj2wAAAAAAUHZdKPx/AACNC7qJYH8AAAMAAAAAAAAANougiWB/AACYgl0o/H8AAAAAAAAAAAAA/wAAAPx/V0Zgo9sAAAAAAEh5XSj8fwAAEMPVAAAAAAM="
},
{
"Id":
"GetCustomIndex:ClassId=0xff,SubclassId=0x47,ProtocolId=0x55",
"Data": "BA=="
},
{
"Id":
"ControlTransfer:Direction=0x00,RequestType=0x02,Recipient=0x00,Request=0x15,Value=0x0000,Idx=0x0007,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,Length=0x4e0",
"Data":
"CgAAAAAAAwbgBAgAAQAAANYECAACAAAAkgGAAAQAAQAoAFUAVgBDAC0ARgBTAFMAZQBuAHMAbwByAEcAcgBvAHUAcABJAEQAAABOAHsARgA2ADYARgBBADYANwA0AC0ARgAyAEIARQAtADQARAAxADkALQA5ADgAQwA1AC0ARAAxADMAMgAzADIAOQBDADYANAAyAEYAfQAAAF4ABAABACwAVQBWAEMALQBGAFMAUwBlAG4AcwBvAHIARwByAG8AdQBwAE4AYQBtAGUAAAAoAEwAZQBuAG8AdgBvACAAQwBhAG0AZQByAGEAIABHAHIAbwB1AHAAAAA8AAQABAAuAFUAVgBDAC0ARQBuAGEAYgBsAGUAUABsAGEAdABmAG8AcgBtAEQAbQBmAHQAAAAEAAEAAAA+AAQABAAwAEUAbgBhAGIAbABlAEQAcwBoAG8AdwBSAGUAZABpAHIAZQBjAHQAaQBvAG4AAAAAAAQAAQAAADIABAAEACQAVQBWAEMALQBDAFAAVgAyAEYAYQBjAGUAQQB1AHQAaAAAAAAABAD//wAACAACAAIAwAGAAAQAAQAoAFUAVgBDAC0ARgBTAFMAZQBuAHMAbwByAEcAcgBvAHUAcABJAEQAAABOAHsARgA2ADYARgBBADYANwA0AC0ARgAyAEIARQAtADQARAAxADkALQA5ADgAQwA1AC0ARAAxADMAMgAzADIAOQBDADYANAAyAEYAfQAAAF4ABAABACwAVQBWAEMALQBGAFMAUwBlAG4AcwBvAHIARwByAG8AdQBwAE4AYQBtAGUAAAAoAEwAZQBuAG8AdgBvACAAQwBhAG0AZQByAGEAIABHAHIAbwB1AHAAAAAwAAQABAAiAFMAZQBuAHMAbwByAEMAYQBtAGUAcgBhAE0AbwBkAGUAAAAEAAEAAAA6AAQABAAsAFMAawBpAHAAQwBhAG0AZQByAGEARQBuAHUAbQBlAHIAYQB0AGkAbwBuAAAABAABAAAAPgAEAAQAMABFAG4AYQBiAGwAZQBEAHMAaABvAHcAUgBlAGQAaQByAGUAYwB0AGkAbwBuAAAAAAAEAAEAAAAyAAQABAAkAFUAVgBDAC0AQwBQAFYAMgBGAGEAYwBlAEEAdQB0AGgAAAAAAAQAAAD//wgAAgAEAHwBMgAEAAQAJABEAGUAdgBpAGMAZQBJAGQAbABlAEUAbgBhAGIAbABlAGQAAAAEAAEAAAAyAAQABAAkAEQAZQBmAGEAdQBsAHQASQBkAGwAZQBTAHQAYQB0AGUAAAAAAAQAAQAAADYABAAEACgARABlAGYAYQB1AGwAdABJAGQAbABlAFQAaQBtAGUAbwB1AHQAAAAAAAQAiBMAAEYABAAEADgARABlAHYAaQBjAGUASQBkAGwAZQBJAGcAbgBvAHIAZQBXAGEAawBlAEUAbgBhAGIAbABlAAAAAAAEAAEAAAAUAAMAV0lOVVNCAAAAAAAAAAAAAIAABAABACgARABlAHYAaQBjAGUASQBuAHQAZQByAGYAYQBjAGUARwBVAEkARAAAAE4AewBlAGMAYwBlAGYAZgAzADUALQAxADQANgAzAC0ANABmAGYAMwAtAGEAYwBkADkALQA4AGYAOQA5ADIAZAAwADkAYQBjAGQAZAB9AAAA"
},
{
"Id":
"ControlTransfer:Direction=0x00,RequestType=0x02,Recipient=0x00,Request=0x2a,Value=0x0000,Idx=0x0007,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=,Length=0x20",
"Data": "UGx1Z2luPWRmdQpJY29uPWNvbXB1dGVyCgAAAAAAAAA="
},
{
"Id": "GetStringDescriptor:DescIndex=0x04",
"Data":
"MjA4MmI1ZTAtN2E2NC00NzhhLWIxYjItZTM0MDRmYWI2ZGFkAAAAAICg2QAAAAAAUHZdKPx/AACNC7qJYH8AAAQAAAAAAAAANougiWB/AAAAsAOKYH8AAAAAAAAAAAAA/wAAAAAAVUeAoNkAAAAAAFB2XSj8fwAAsKPbAAAAAAQ="
}
]
}
]
}

View File

@ -1,4 +1,4 @@
[wrap-git]
directory = gusb
url = https://github.com/hughsie/libgusb.git
revision = 0.3.10
revision = 0.4.0