fwupd/libfwupdplugin/fu-quirks.c
Richard Hughes 6235a8222a Share the Common Flash Memory Interface quirks between plugins
Now two plugins are using hardcoded SPI constants for various CFI chips,
and it makes sense to have some common quirk data that can be used by
both.

Add a FuSpiChip helper object that can be used by FuDevice subclasses
to get the specific SPI commands to use for each flash ID.
2021-10-20 12:19:39 +01:00

558 lines
17 KiB
C

/*
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#define G_LOG_DOMAIN "FuQuirks"
#include "config.h"
#include <gio/gio.h>
#include <glib-object.h>
#include <string.h>
#include <xmlb.h>
#include "fwupd-common.h"
#include "fwupd-error.h"
#include "fwupd-remote-private.h"
#include "fu-common.h"
#include "fu-mutex.h"
#include "fu-quirks.h"
/**
* FuQuirks:
*
* Quirks can be used to modify device behavior.
* When fwupd is installed in long-term support distros it's very hard to
* backport new versions as new hardware is released.
*
* There are several reasons why we can't just include the mapping and quirk
* information in the AppStream metadata:
*
* * The extra data is hugely specific to the installed fwupd plugin versions
* * The device-id is per-device, and the mapping is usually per-plugin
* * Often the information is needed before the FuDevice is created
* * There are security implications in allowing plugins to handle new devices
*
* The idea with quirks is that the end user can drop an additional (or replace
* an existing) file in a .d director with a simple format and the hardware will
* magically start working. This assumes no new quirks are required, as this would
* obviously need code changes, but allows us to get most existing devices working
* in an easy way without the user compiling anything.
*
* See also: [class@FuDevice], [class@FuPlugin]
*/
static void
fu_quirks_finalize(GObject *obj);
struct _FuQuirks {
GObject parent_instance;
FuQuirksLoadFlags load_flags;
GHashTable *possible_keys;
GPtrArray *invalid_keys;
XbSilo *silo;
};
G_DEFINE_TYPE(FuQuirks, fu_quirks, G_TYPE_OBJECT)
static gchar *
fu_quirks_build_group_key(const gchar *group)
{
const gchar *guid_prefixes[] = {"DeviceInstanceId=", "Guid=", "HwId=", NULL};
/* this is a GUID */
for (guint i = 0; guid_prefixes[i] != NULL; i++) {
if (g_str_has_prefix(group, guid_prefixes[i])) {
gsize len = strlen(guid_prefixes[i]);
g_warning("using %s for %s in quirk files is deprecated!",
guid_prefixes[i],
group);
if (fwupd_guid_is_valid(group + len))
return g_strdup(group + len);
return fwupd_guid_hash_string(group + len);
}
}
/* fallback */
if (fwupd_guid_is_valid(group))
return g_strdup(group);
return fwupd_guid_hash_string(group);
}
static GInputStream *
fu_quirks_convert_quirk_to_xml_cb(XbBuilderSource *source,
XbBuilderSourceCtx *ctx,
gpointer user_data,
GCancellable *cancellable,
GError **error)
{
FuQuirks *self = FU_QUIRKS(user_data);
g_autofree gchar *xml = NULL;
g_auto(GStrv) groups = NULL;
g_autoptr(GBytes) bytes = NULL;
g_autoptr(GKeyFile) kf = g_key_file_new();
g_autoptr(XbBuilderNode) root = xb_builder_node_new("quirk");
/* parse keyfile */
bytes = xb_builder_source_ctx_get_bytes(ctx, cancellable, error);
if (bytes == NULL)
return NULL;
if (!g_key_file_load_from_data(kf,
g_bytes_get_data(bytes, NULL),
g_bytes_get_size(bytes),
G_KEY_FILE_NONE,
error))
return NULL;
/* add each set of groups and keys */
groups = g_key_file_get_groups(kf, NULL);
for (guint i = 0; groups[i] != NULL; i++) {
g_auto(GStrv) keys = NULL;
g_autofree gchar *group_id = NULL;
g_autoptr(XbBuilderNode) bn = NULL;
/* sanity check group */
if (g_str_has_prefix(groups[i], "HwID") ||
g_str_has_prefix(groups[i], "DeviceInstanceID") ||
g_str_has_prefix(groups[i], "GUID")) {
g_warning("invalid group name '%s'", groups[i]);
continue;
}
/* get all KVs for the entry */
keys = g_key_file_get_keys(kf, groups[i], NULL, error);
if (keys == NULL)
return NULL;
group_id = fu_quirks_build_group_key(groups[i]);
bn = xb_builder_node_insert(root, "device", "id", group_id, NULL);
for (guint j = 0; keys[j] != NULL; j++) {
g_autofree gchar *value = NULL;
/* sanity check key */
if ((self->load_flags & FU_QUIRKS_LOAD_FLAG_NO_VERIFY) == 0 &&
g_hash_table_lookup(self->possible_keys, keys[j]) == NULL) {
if (!g_ptr_array_find_with_equal_func(self->invalid_keys,
keys[j],
g_str_equal,
NULL)) {
g_ptr_array_add(self->invalid_keys, g_strdup(keys[j]));
}
}
value = g_key_file_get_value(kf, groups[i], keys[j], error);
if (value == NULL)
return NULL;
xb_builder_node_insert_text(bn, "value", value, "key", keys[j], NULL);
}
}
/* export as XML */
xml = xb_builder_node_export(root, XB_NODE_EXPORT_FLAG_ADD_HEADER, error);
if (xml == NULL)
return NULL;
return g_memory_input_stream_new_from_data(g_steal_pointer(&xml), -1, g_free);
}
static gint
fu_quirks_filename_sort_cb(gconstpointer a, gconstpointer b)
{
const gchar *stra = *((const gchar **)a);
const gchar *strb = *((const gchar **)b);
return g_strcmp0(stra, strb);
}
static gboolean
fu_quirks_add_quirks_for_path(FuQuirks *self, XbBuilder *builder, const gchar *path, GError **error)
{
const gchar *tmp;
g_autoptr(GDir) dir = NULL;
g_autoptr(GPtrArray) filenames = g_ptr_array_new_with_free_func(g_free);
/* add valid files to the array */
if (!g_file_test(path, G_FILE_TEST_EXISTS))
return TRUE;
dir = g_dir_open(path, 0, error);
if (dir == NULL)
return FALSE;
while ((tmp = g_dir_read_name(dir)) != NULL) {
if (!g_str_has_suffix(tmp, ".quirk")) {
g_debug("skipping invalid file %s", tmp);
continue;
}
g_ptr_array_add(filenames, g_build_filename(path, tmp, NULL));
}
/* sort */
g_ptr_array_sort(filenames, fu_quirks_filename_sort_cb);
/* process files */
for (guint i = 0; i < filenames->len; i++) {
const gchar *filename = g_ptr_array_index(filenames, i);
g_autoptr(GFile) file = g_file_new_for_path(filename);
g_autoptr(XbBuilderSource) source = xb_builder_source_new();
/* load from keyfile */
#if LIBXMLB_CHECK_VERSION(0, 1, 15)
xb_builder_source_add_simple_adapter(source,
"text/plain,.quirk",
fu_quirks_convert_quirk_to_xml_cb,
self,
NULL);
#else
xb_builder_source_add_adapter(source,
"text/plain,.quirk",
fu_quirks_convert_quirk_to_xml_cb,
self,
NULL);
#endif
if (!xb_builder_source_load_file(source,
file,
XB_BUILDER_SOURCE_FLAG_WATCH_FILE |
XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT,
NULL,
error)) {
g_prefix_error(error, "failed to load %s: ", filename);
return FALSE;
}
/* watch the file for changes */
xb_builder_import_source(builder, source);
}
/* success */
return TRUE;
}
static gint
fu_quirks_strcasecmp_cb(gconstpointer a, gconstpointer b)
{
const gchar *entry1 = *((const gchar **)a);
const gchar *entry2 = *((const gchar **)b);
return g_ascii_strcasecmp(entry1, entry2);
}
static gboolean
fu_quirks_check_silo(FuQuirks *self, GError **error)
{
XbBuilderCompileFlags compile_flags = XB_BUILDER_COMPILE_FLAG_WATCH_BLOB;
g_autofree gchar *datadir = NULL;
g_autofree gchar *localstatedir = NULL;
g_autoptr(GFile) file = NULL;
g_autoptr(XbBuilder) builder = NULL;
/* everything is okay */
if (self->silo != NULL && xb_silo_is_valid(self->silo))
return TRUE;
/* system datadir */
builder = xb_builder_new();
datadir = fu_common_get_path(FU_PATH_KIND_DATADIR_QUIRKS);
if (!fu_quirks_add_quirks_for_path(self, builder, datadir, error))
return FALSE;
/* something we can write when using Ostree */
localstatedir = fu_common_get_path(FU_PATH_KIND_LOCALSTATEDIR_QUIRKS);
if (!fu_quirks_add_quirks_for_path(self, builder, localstatedir, error))
return FALSE;
/* load silo */
if (self->load_flags & FU_QUIRKS_LOAD_FLAG_NO_CACHE) {
g_autoptr(GFileIOStream) iostr = NULL;
file = g_file_new_tmp(NULL, &iostr, error);
if (file == NULL)
return FALSE;
} else {
g_autofree gchar *cachedirpkg = fu_common_get_path(FU_PATH_KIND_CACHEDIR_PKG);
g_autofree gchar *xmlbfn = g_build_filename(cachedirpkg, "quirks.xmlb", NULL);
file = g_file_new_for_path(xmlbfn);
}
if (g_getenv("FWUPD_XMLB_VERBOSE") != NULL) {
xb_builder_set_profile_flags(builder,
XB_SILO_PROFILE_FLAG_XPATH |
XB_SILO_PROFILE_FLAG_DEBUG);
}
if (self->load_flags & FU_QUIRKS_LOAD_FLAG_READONLY_FS)
compile_flags |= XB_BUILDER_COMPILE_FLAG_IGNORE_GUID;
self->silo = xb_builder_ensure(builder, file, compile_flags, NULL, error);
if (self->silo == NULL)
return FALSE;
/* dump warnings to console, just once */
if (self->invalid_keys->len > 0) {
g_autofree gchar *str = NULL;
g_ptr_array_sort(self->invalid_keys, fu_quirks_strcasecmp_cb);
str = fu_common_strjoin_array(",", self->invalid_keys);
g_debug("invalid key names: %s", str);
}
/* success */
return TRUE;
}
/**
* fu_quirks_lookup_by_id:
* @self: a #FuQuirks
* @group: a string group, e.g. `DeviceInstanceId=USB\VID_1235&PID_AB11`
* @key: an ID to match the entry, e.g. `Name`
*
* Looks up an entry in the hardware database using a string value.
*
* Returns: (transfer none): values from the database, or %NULL if not found
*
* Since: 1.0.1
**/
const gchar *
fu_quirks_lookup_by_id(FuQuirks *self, const gchar *group, const gchar *key)
{
g_autofree gchar *group_key = NULL;
g_autoptr(GError) error = NULL;
g_autoptr(XbNode) n = NULL;
g_autoptr(XbQuery) query = NULL;
#if LIBXMLB_CHECK_VERSION(0, 3, 0)
g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT();
#endif
g_return_val_if_fail(FU_IS_QUIRKS(self), NULL);
g_return_val_if_fail(group != NULL, NULL);
g_return_val_if_fail(key != NULL, NULL);
/* ensure up to date */
if (!fu_quirks_check_silo(self, &error)) {
g_warning("failed to build silo: %s", error->message);
return NULL;
}
/* query */
group_key = fu_quirks_build_group_key(group);
query = xb_query_new_full(self->silo,
"quirk/device[@id=?]/value[@key=?]",
XB_QUERY_FLAG_NONE,
&error);
if (query == NULL) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
return NULL;
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
return NULL;
g_warning("failed to build query: %s", error->message);
return NULL;
}
#if LIBXMLB_CHECK_VERSION(0, 3, 0)
xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, group_key, NULL);
xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 1, key, NULL);
n = xb_silo_query_first_with_context(self->silo, query, &context, &error);
#else
if (!xb_query_bind_str(query, 0, group_key, &error)) {
g_warning("failed to bind 0: %s", error->message);
return NULL;
}
if (!xb_query_bind_str(query, 1, key, &error)) {
g_warning("failed to bind 1: %s", error->message);
return NULL;
}
n = xb_silo_query_first_full(self->silo, query, &error);
#endif
if (n == NULL) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
return NULL;
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
return NULL;
g_warning("failed to query: %s", error->message);
return NULL;
}
return xb_node_get_text(n);
}
/**
* fu_quirks_lookup_by_id_iter:
* @self: a #FuQuirks
* @group: string of group to lookup
* @iter_cb: (scope async): a function to call for each result
* @user_data: user data passed to @iter_cb
*
* Looks up all entries in the hardware database using a GUID value.
*
* Returns: %TRUE if the ID was found, and @iter was called
*
* Since: 1.3.3
**/
gboolean
fu_quirks_lookup_by_id_iter(FuQuirks *self,
const gchar *group,
FuQuirksIter iter_cb,
gpointer user_data)
{
g_autofree gchar *group_key = NULL;
g_autoptr(GError) error = NULL;
g_autoptr(GPtrArray) results = NULL;
g_autoptr(XbQuery) query = NULL;
#if LIBXMLB_CHECK_VERSION(0, 3, 0)
g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT();
#endif
g_return_val_if_fail(FU_IS_QUIRKS(self), FALSE);
g_return_val_if_fail(group != NULL, FALSE);
g_return_val_if_fail(iter_cb != NULL, FALSE);
/* ensure up to date */
if (!fu_quirks_check_silo(self, &error)) {
g_warning("failed to build silo: %s", error->message);
return FALSE;
}
/* query */
group_key = fu_quirks_build_group_key(group);
query =
xb_query_new_full(self->silo, "quirk/device[@id=?]/value", XB_QUERY_FLAG_NONE, &error);
if (query == NULL) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
return FALSE;
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
return FALSE;
g_warning("failed to build query: %s", error->message);
return FALSE;
}
#if LIBXMLB_CHECK_VERSION(0, 3, 0)
xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, group_key, NULL);
results = xb_silo_query_with_context(self->silo, query, &context, &error);
#else
if (!xb_query_bind_str(query, 0, group_key, &error)) {
g_warning("failed to bind 0: %s", error->message);
return FALSE;
}
results = xb_silo_query_full(self->silo, query, &error);
#endif
if (results == NULL) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
return FALSE;
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
return FALSE;
g_warning("failed to query: %s", error->message);
return FALSE;
}
for (guint i = 0; i < results->len; i++) {
XbNode *n = g_ptr_array_index(results, i);
iter_cb(self, xb_node_get_attr(n, "key"), xb_node_get_text(n), user_data);
}
return TRUE;
}
/**
* fu_quirks_load: (skip)
* @self: a #FuQuirks
* @load_flags: load flags
* @error: (nullable): optional return location for an error
*
* Loads the various files that define the hardware quirks used in plugins.
*
* Returns: %TRUE for success
*
* Since: 1.0.1
**/
gboolean
fu_quirks_load(FuQuirks *self, FuQuirksLoadFlags load_flags, GError **error)
{
g_return_val_if_fail(FU_IS_QUIRKS(self), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
self->load_flags = load_flags;
return fu_quirks_check_silo(self, error);
}
/**
* fu_quirks_add_possible_key:
* @self: a #FuQuirks
* @possible_key: a key name, e.g. `Flags`
*
* Adds a possible quirk key. If added by a plugin it should be namespaced
* using the plugin name, where possible.
*
* Since: 1.5.8
**/
void
fu_quirks_add_possible_key(FuQuirks *self, const gchar *possible_key)
{
g_return_if_fail(FU_IS_QUIRKS(self));
g_return_if_fail(possible_key != NULL);
g_hash_table_add(self->possible_keys, g_strdup(possible_key));
}
static void
fu_quirks_class_init(FuQuirksClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fu_quirks_finalize;
}
static void
fu_quirks_init(FuQuirks *self)
{
self->possible_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
self->invalid_keys = g_ptr_array_new_with_free_func(g_free);
/* built in */
fu_quirks_add_possible_key(self, FU_QUIRKS_BRANCH);
fu_quirks_add_possible_key(self, FU_QUIRKS_CHILDREN);
fu_quirks_add_possible_key(self, FU_QUIRKS_COUNTERPART_GUID);
fu_quirks_add_possible_key(self, FU_QUIRKS_FIRMWARE_SIZE);
fu_quirks_add_possible_key(self, FU_QUIRKS_FIRMWARE_SIZE_MAX);
fu_quirks_add_possible_key(self, FU_QUIRKS_FIRMWARE_SIZE_MIN);
fu_quirks_add_possible_key(self, FU_QUIRKS_FLAGS);
fu_quirks_add_possible_key(self, FU_QUIRKS_GTYPE);
fu_quirks_add_possible_key(self, FU_QUIRKS_GUID);
fu_quirks_add_possible_key(self, FU_QUIRKS_ICON);
fu_quirks_add_possible_key(self, FU_QUIRKS_INHIBIT);
fu_quirks_add_possible_key(self, FU_QUIRKS_INSTALL_DURATION);
fu_quirks_add_possible_key(self, FU_QUIRKS_NAME);
fu_quirks_add_possible_key(self, FU_QUIRKS_PARENT_GUID);
fu_quirks_add_possible_key(self, FU_QUIRKS_PLUGIN);
fu_quirks_add_possible_key(self, FU_QUIRKS_PRIORITY);
fu_quirks_add_possible_key(self, FU_QUIRKS_PROTOCOL);
fu_quirks_add_possible_key(self, FU_QUIRKS_PROXY_GUID);
fu_quirks_add_possible_key(self, FU_QUIRKS_BATTERY_THRESHOLD);
fu_quirks_add_possible_key(self, FU_QUIRKS_REMOVE_DELAY);
fu_quirks_add_possible_key(self, FU_QUIRKS_SUMMARY);
fu_quirks_add_possible_key(self, FU_QUIRKS_UPDATE_IMAGE);
fu_quirks_add_possible_key(self, FU_QUIRKS_UPDATE_MESSAGE);
fu_quirks_add_possible_key(self, FU_QUIRKS_VENDOR);
fu_quirks_add_possible_key(self, FU_QUIRKS_VENDOR_ID);
fu_quirks_add_possible_key(self, FU_QUIRKS_VERSION);
fu_quirks_add_possible_key(self, FU_QUIRKS_VERSION_FORMAT);
fu_quirks_add_possible_key(self, "CfiDeviceCmdReadId");
fu_quirks_add_possible_key(self, "CfiDeviceCmdReadIdSz");
fu_quirks_add_possible_key(self, "CfiDeviceCmdChipErase");
fu_quirks_add_possible_key(self, "CfiDeviceCmdSectorErase");
}
static void
fu_quirks_finalize(GObject *obj)
{
FuQuirks *self = FU_QUIRKS(obj);
if (self->silo != NULL)
g_object_unref(self->silo);
g_hash_table_unref(self->possible_keys);
g_ptr_array_unref(self->invalid_keys);
G_OBJECT_CLASS(fu_quirks_parent_class)->finalize(obj);
}
/**
* fu_quirks_new: (skip)
*
* Creates a new quirks object.
*
* Returns: a new #FuQuirks
*
* Since: 1.0.1
**/
FuQuirks *
fu_quirks_new(void)
{
FuQuirks *self;
self = g_object_new(FU_TYPE_QUIRKS, NULL);
return FU_QUIRKS(self);
}