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