mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-24 21:57:48 +00:00

This replaces fwupd-dfu-alternate-vidpid which was only useful in the DFU plugin and somewhat poorly defined.
370 lines
11 KiB
C
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);
|
|
}
|