/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2017 Richard Hughes * * 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 #include #include #include #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); }