/* * Copyright (C) 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #define G_LOG_DOMAIN "FuQuirks" #include "config.h" #include #include #include #include #include "fu-common.h" #include "fu-mutex.h" #include "fu-quirks.h" #include "fwupd-common.h" #include "fwupd-error.h" #include "fwupd-remote-private.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_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)) 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); 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_PKG); 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_PKG); 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); } 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); }