fwupd/src/fu-quirks.c
Richard Hughes e755d6bb05 Add a more general quirk for adding GUIDs to devices
This replaces fwupd-dfu-alternate-vidpid which was only useful in the DFU plugin
and somewhat poorly defined.
2017-12-04 17:00:22 +00:00

370 lines
11 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
*
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
*
* Licensed under the GNU General Public License Version 2
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "config.h"
#include <glib-object.h>
#include <gio/gio.h>
#include <fnmatch.h>
#include <string.h>
#include "fu-quirks.h"
#include "fwupd-error.h"
#include "fwupd-remote-private.h"
/**
* SECTION:fu-quirks
* @short_description: device quirks
*
* Quirks can be used to modify device behaviour.
* 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: #FuDevice, #FuPlugin
*/
static void fu_quirks_finalize (GObject *obj);
struct _FuQuirks
{
GObject parent_instance;
GPtrArray *monitors;
GHashTable *hash; /* of prefix/id:string */
};
G_DEFINE_TYPE (FuQuirks, fu_quirks, G_TYPE_OBJECT)
static void
fu_quirks_monitor_changed_cb (GFileMonitor *monitor,
GFile *file,
GFile *other_file,
GFileMonitorEvent event_type,
gpointer user_data)
{
FuQuirks *self = FU_QUIRKS (user_data);
g_autoptr(GError) error = NULL;
g_autofree gchar *filename = g_file_get_path (file);
g_debug ("%s changed, reloading all configs", filename);
if (!fu_quirks_load (self, &error))
g_warning ("failed to rescan quirks: %s", error->message);
}
static gboolean
fu_quirks_add_inotify (FuQuirks *self, const gchar *filename, GError **error)
{
GFileMonitor *monitor;
g_autoptr(GFile) file = g_file_new_for_path (filename);
/* set up a notify watch */
monitor = g_file_monitor (file, G_FILE_MONITOR_NONE, NULL, error);
if (monitor == NULL)
return FALSE;
g_signal_connect (monitor, "changed",
G_CALLBACK (fu_quirks_monitor_changed_cb), self);
g_ptr_array_add (self->monitors, monitor);
return TRUE;
}
/**
* fu_quirks_lookup_by_id:
* @self: A #FuPlugin
* @prefix: A string prefix that matches the quirks file basename, e.g. "dfu-quirks"
* @id: An ID to match the entry, e.g. "012345"
*
* 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 *prefix, const gchar *id)
{
g_autofree gchar *key = NULL;
g_return_val_if_fail (FU_IS_QUIRKS (self), NULL);
g_return_val_if_fail (prefix != NULL, NULL);
g_return_val_if_fail (id != NULL, NULL);
key = g_strdup_printf ("%s/%s", prefix, id);
return g_hash_table_lookup (self->hash, key);
}
/**
* fu_quirks_lookup_by_glob:
* @self: A #FuPlugin
* @prefix: A string prefix that matches the quirks file basename, e.g. "dfu-quirks"
* @glob: An glob to match the entry, e.g. "foo*bar?baz"
*
* Looks up an entry in the hardware database using a key glob.
* NOTE: This is *much* slower than using fu_quirks_lookup_by_id() as each key
* in the quirk database is compared.
*
* Returns: (transfer none): values from the database, or %NULL if not found
*
* Since: 1.0.1
**/
const gchar *
fu_quirks_lookup_by_glob (FuQuirks *self, const gchar *prefix, const gchar *glob)
{
g_autoptr(GList) keys = NULL;
gsize prefix_len;
g_return_val_if_fail (FU_IS_QUIRKS (self), NULL);
g_return_val_if_fail (prefix != NULL, NULL);
g_return_val_if_fail (glob != NULL, NULL);
prefix_len = strlen (prefix);
keys = g_hash_table_get_keys (self->hash);
for (GList *l = keys; l != NULL; l = l->next) {
const gchar *id = l->data;
if (strncmp (id, prefix, prefix_len) != 0)
continue;
id += prefix_len + 1;
if (fnmatch (glob, id, 0) == 0)
return fu_quirks_lookup_by_id (self, prefix, id);
if (fnmatch (id, glob, 0) == 0)
return fu_quirks_lookup_by_id (self, prefix, id);
}
return NULL;
}
/**
* fu_quirks_lookup_by_usb_device:
* @self: A #FuPlugin
* @prefix: A string prefix that matches the quirks file basename, e.g. "dfu-quirks"
* @dev: A #GUsbDevice
*
* Looks up an entry in the hardware database using various keys generated
* from @dev.
*
* Returns: (transfer none): values from the database, or %NULL if not found
*
* Since: 1.0.1
**/
const gchar *
fu_quirks_lookup_by_usb_device (FuQuirks *self, const gchar *prefix, GUsbDevice *dev)
{
const gchar *tmp;
g_autofree gchar *key1 = NULL;
g_autofree gchar *key2 = NULL;
g_autofree gchar *key3 = NULL;
g_return_val_if_fail (FU_IS_QUIRKS (self), NULL);
g_return_val_if_fail (prefix != NULL, NULL);
g_return_val_if_fail (G_USB_IS_DEVICE (dev), NULL);
/* prefer an exact match, VID:PID:REV */
key1 = g_strdup_printf ("USB\\VID_%04X&PID_%04X&REV_%04X",
g_usb_device_get_vid (dev),
g_usb_device_get_pid (dev),
g_usb_device_get_release (dev));
tmp = fu_quirks_lookup_by_id (self, prefix, key1);
if (tmp != NULL)
return tmp;
/* VID:PID */
key2 = g_strdup_printf ("USB\\VID_%04X&PID_%04X",
g_usb_device_get_vid (dev),
g_usb_device_get_pid (dev));
tmp = fu_quirks_lookup_by_id (self, prefix, key2);
if (tmp != NULL)
return tmp;
/* VID */
key3 = g_strdup_printf ("USB\\VID_%04X", g_usb_device_get_vid (dev));
return fu_quirks_lookup_by_id (self, prefix, key3);
}
static gboolean
fu_quirks_add_quirks_from_filename (FuQuirks *self, const gchar *filename, GError **error)
{
g_autoptr(GKeyFile) kf = g_key_file_new ();
g_auto(GStrv) groups = NULL;
/* load keyfile */
if (!g_key_file_load_from_file (kf, filename, G_KEY_FILE_NONE, error))
return FALSE;
/* 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;
keys = g_key_file_get_keys (kf, groups[i], NULL, error);
if (keys == NULL)
return FALSE;
for (guint j = 0; keys[j] != NULL; j++) {
g_autofree gchar *tmp = NULL;
tmp = g_key_file_get_value (kf, groups[i], keys[j], error);
if (tmp == NULL)
return FALSE;
g_hash_table_insert (self->hash,
g_strdup_printf ("%s/%s", groups[i], keys[j]),
g_steal_pointer (&tmp));
}
}
g_debug ("now %u quirk entries", g_hash_table_size (self->hash));
return TRUE;
}
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, const gchar *path, GError **error)
{
const gchar *tmp;
g_autofree gchar *path_hw = NULL;
g_autoptr(GDir) dir = NULL;
g_autoptr(GPtrArray) filenames = g_ptr_array_new_with_free_func (g_free);
/* add valid files to the array */
path_hw = g_build_filename (path, "quirks.d", NULL);
if (!g_file_test (path_hw, G_FILE_TEST_EXISTS)) {
g_debug ("no %s, skipping", path_hw);
return TRUE;
}
dir = g_dir_open (path_hw, 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_hw, 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);
/* load from keyfile */
g_debug ("loading quirks from %s", filename);
if (!fu_quirks_add_quirks_from_filename (self, filename, error)) {
g_prefix_error (error, "failed to load %s: ", filename);
return FALSE;
}
/* watch the file for changes */
if (!fu_quirks_add_inotify (self, filename, error))
return FALSE;
}
/* success */
return TRUE;
}
/**
* fu_quirks_load: (skip)
* @self: A #FuQuirks
* @error: A #GError, or %NULL
*
* 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, GError **error)
{
g_autofree gchar *localstate_fwupd = NULL;
g_return_val_if_fail (FU_IS_QUIRKS (self), FALSE);
/* ensure empty in case we're called from a monitor change */
g_ptr_array_set_size (self->monitors, 0);
g_hash_table_remove_all (self->hash);
/* system datadir */
if (!fu_quirks_add_quirks_for_path (self, FWUPDDATADIR, error))
return FALSE;
/* something we can write when using Ostree */
localstate_fwupd = g_build_filename (LOCALSTATEDIR, "lib", "fwupd", NULL);
if (!fu_quirks_add_quirks_for_path (self, localstate_fwupd, error))
return FALSE;
/* success */
return TRUE;
}
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->monitors = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
self->hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
}
static void
fu_quirks_finalize (GObject *obj)
{
FuQuirks *self = FU_QUIRKS (obj);
g_ptr_array_unref (self->monitors);
g_hash_table_unref (self->hash);
G_OBJECT_CLASS (fu_quirks_parent_class)->finalize (obj);
}
/**
* fu_quirks_new: (skip)
*
* Creates a new quirks object.
*
* Return value: a new #FuQuirks
**/
FuQuirks *
fu_quirks_new (void)
{
FuQuirks *self;
self = g_object_new (FU_TYPE_QUIRKS, NULL);
return FU_QUIRKS (self);
}