blob: b9d8f7aa327dd51f17b43215f0e55689b883b986 [file] [log] [blame]
/*
* Copyright 2024 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#define G_LOG_DOMAIN "FuEngine"
#include "config.h"
#include "fu-archive.h"
#include "fu-context-private.h"
#include "fu-device-private.h"
#include "fu-engine-emulator.h"
struct _FuEngineEmulator {
GObject parent_instance;
FuEngine *engine;
GHashTable *phase_blobs; /* (element-type utf-8 GBytes) */
};
G_DEFINE_TYPE(FuEngineEmulator, fu_engine_emulator, G_TYPE_OBJECT)
enum { PROP_0, PROP_ENGINE, PROP_LAST };
static gchar *
fu_engine_emulator_phase_to_filename(FuEngineEmulatorPhase phase, guint write_cnt)
{
if (write_cnt == FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT)
return g_strdup_printf("%s.json", fu_engine_emulator_phase_to_string(phase));
return g_strdup_printf("%s-%u.json", fu_engine_emulator_phase_to_string(phase), write_cnt);
}
gboolean
fu_engine_emulator_save(FuEngineEmulator *self, GOutputStream *stream, GError **error)
{
GHashTableIter iter;
gboolean got_json = FALSE;
gpointer key;
gpointer value;
g_autoptr(GByteArray) buf = NULL;
g_autoptr(GBytes) blob = NULL;
g_autoptr(FuArchive) archive = fu_archive_new(NULL, FU_ARCHIVE_FLAG_NONE, NULL);
g_return_val_if_fail(FU_IS_ENGINE_EMULATOR(self), FALSE);
g_return_val_if_fail(G_IS_OUTPUT_STREAM(stream), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* sanity check */
g_hash_table_iter_init(&iter, self->phase_blobs);
while (g_hash_table_iter_next(&iter, &key, &value)) {
fu_archive_add_entry(archive, (const gchar *)key, (GBytes *)value);
got_json = TRUE;
}
if (!got_json) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"no emulation data, perhaps no devices have been added?");
return FALSE;
}
/* write */
buf = fu_archive_write(archive, FU_ARCHIVE_FORMAT_ZIP, FU_ARCHIVE_COMPRESSION_GZIP, error);
if (buf == NULL)
return FALSE;
blob = g_byte_array_free_to_bytes(g_steal_pointer(&buf)); /* nocheck:blocked */
if (!fu_output_stream_write_bytes(stream, blob, NULL, error))
return FALSE;
if (!g_output_stream_flush(stream, NULL, error)) {
fwupd_error_convert(error);
return FALSE;
}
/* success */
g_hash_table_remove_all(self->phase_blobs);
return TRUE;
}
static gboolean
fu_engine_emulator_load_json_blob(FuEngineEmulator *self, GBytes *json_blob, GError **error)
{
GPtrArray *backends = fu_context_get_backends(fu_engine_get_context(self->engine));
JsonNode *root;
g_autoptr(JsonParser) parser = json_parser_new();
/* parse */
if (!json_parser_load_from_data(parser,
g_bytes_get_data(json_blob, NULL),
g_bytes_get_size(json_blob),
error))
return FALSE;
/* load into all backends */
root = json_parser_get_root(parser);
for (guint i = 0; i < backends->len; i++) {
FuBackend *backend = g_ptr_array_index(backends, i);
if (!fwupd_codec_from_json(FWUPD_CODEC(backend), root, error))
return FALSE;
}
/* success */
return TRUE;
}
gboolean
fu_engine_emulator_load_phase(FuEngineEmulator *self,
FuEngineEmulatorPhase phase,
guint write_cnt,
GError **error)
{
GBytes *json_blob;
g_autofree gchar *fn = fu_engine_emulator_phase_to_filename(phase, write_cnt);
json_blob = g_hash_table_lookup(self->phase_blobs, fn);
if (json_blob == NULL)
return TRUE;
return fu_engine_emulator_load_json_blob(self, json_blob, error);
}
static void
fu_engine_emulator_to_json(FuEngineEmulator *self, GPtrArray *devices, JsonBuilder *json_builder)
{
/* not always correct, but we want to remain compatible with all the old emulation files */
json_builder_begin_object(json_builder);
fwupd_codec_json_append(json_builder, "FwupdVersion", PACKAGE_VERSION);
json_builder_set_member_name(json_builder, "UsbDevices");
json_builder_begin_array(json_builder);
for (guint i = 0; i < devices->len; i++) {
FuDevice *device = g_ptr_array_index(devices, i);
/* interesting? */
if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG))
continue;
json_builder_begin_object(json_builder);
fu_device_add_json(device, json_builder, FWUPD_CODEC_FLAG_NONE);
json_builder_end_object(json_builder);
}
json_builder_end_array(json_builder);
json_builder_end_object(json_builder);
/* we've recorded these, now drop them */
for (guint i = 0; i < devices->len; i++) {
FuDevice *device = g_ptr_array_index(devices, i);
if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG))
continue;
fu_device_clear_events(device);
}
}
gboolean
fu_engine_emulator_save_phase(FuEngineEmulator *self,
FuEngineEmulatorPhase phase,
guint write_cnt,
GError **error)
{
GBytes *blob_old;
g_autofree gchar *blob_new_safe = NULL;
g_autofree gchar *fn = fu_engine_emulator_phase_to_filename(phase, write_cnt);
g_autoptr(GBytes) blob_new = NULL;
g_autoptr(GPtrArray) devices = NULL;
g_autoptr(GOutputStream) ostream = g_memory_output_stream_new_resizable();
g_autoptr(JsonBuilder) json_builder = json_builder_new();
g_autoptr(JsonGenerator) json_generator = NULL;
g_autoptr(JsonNode) json_root = NULL;
/* all devices in all backends */
devices = fu_engine_get_devices(self->engine, error);
if (devices == NULL)
return FALSE;
fu_engine_emulator_to_json(self, devices, json_builder);
json_root = json_builder_get_root(json_builder);
json_generator = json_generator_new();
json_generator_set_pretty(json_generator, TRUE);
json_generator_set_root(json_generator, json_root);
blob_old = g_hash_table_lookup(self->phase_blobs, fn);
if (!json_generator_to_stream(json_generator, ostream, NULL, error))
return FALSE;
if (!g_output_stream_close(ostream, NULL, error))
return FALSE;
blob_new = g_memory_output_stream_steal_as_bytes(G_MEMORY_OUTPUT_STREAM(ostream));
if (g_bytes_get_size(blob_new) == 0) {
g_info("no data for phase %s [%u]",
fu_engine_emulator_phase_to_string(phase),
write_cnt);
return TRUE;
}
if (blob_old != NULL && g_bytes_compare(blob_old, blob_new) == 0) {
g_info("JSON unchanged for phase %s [%u]",
fu_engine_emulator_phase_to_string(phase),
write_cnt);
return TRUE;
}
blob_new_safe = fu_strsafe_bytes(blob_new, 8000);
g_info("JSON %s for phase %s [%u]: %s…",
blob_old == NULL ? "added" : "changed",
fu_engine_emulator_phase_to_string(phase),
write_cnt,
blob_new_safe);
g_hash_table_insert(self->phase_blobs, g_steal_pointer(&fn), g_steal_pointer(&blob_new));
/* success */
return TRUE;
}
static gboolean
fu_engine_emulator_load_phases(FuEngineEmulator *self,
FuArchive *archive,
guint write_cnt,
gboolean *got_json,
GError **error)
{
for (FuEngineEmulatorPhase phase = FU_ENGINE_EMULATOR_PHASE_SETUP;
phase < FU_ENGINE_EMULATOR_PHASE_LAST;
phase++) {
g_autofree gchar *fn = fu_engine_emulator_phase_to_filename(phase, write_cnt);
g_autoptr(GBytes) blob = NULL;
/* not found */
blob = fu_archive_lookup_by_fn(archive, fn, NULL);
if (blob == NULL || g_bytes_get_size(blob) == 0)
continue;
*got_json = TRUE;
g_info("emulation for phase %s [%u]",
fu_engine_emulator_phase_to_string(phase),
write_cnt);
if (write_cnt == FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT &&
phase == FU_ENGINE_EMULATOR_PHASE_SETUP) {
if (!fu_engine_emulator_load_json_blob(self, blob, error))
return FALSE;
} else {
g_hash_table_insert(self->phase_blobs,
g_steal_pointer(&fn),
g_steal_pointer(&blob));
}
}
/* success */
return TRUE;
}
gboolean
fu_engine_emulator_load(FuEngineEmulator *self, GInputStream *stream, GError **error)
{
gboolean got_json = FALSE;
const gchar *json_empty = "{\"UsbDevices\":[]}";
g_autoptr(FuArchive) archive = NULL;
g_autoptr(GBytes) json_blob = g_bytes_new_static(json_empty, strlen(json_empty));
g_autoptr(GError) error_archive = NULL;
g_return_val_if_fail(FU_IS_ENGINE_EMULATOR(self), FALSE);
g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* unload any existing devices */
if (!fu_engine_emulator_load_json_blob(self, json_blob, error))
return FALSE;
g_hash_table_remove_all(self->phase_blobs);
/* load archive */
archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_NONE, &error_archive);
if (archive == NULL) {
g_autoptr(GBytes) blob = NULL;
g_debug("no archive found, using JSON as phase setup: %s", error_archive->message);
blob = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error);
if (blob == NULL)
return FALSE;
return fu_engine_emulator_load_json_blob(self, blob, error);
}
/* load JSON files from archive */
for (guint write_cnt = 0; write_cnt < FU_ENGINE_EMULATOR_WRITE_COUNT_MAX; write_cnt++) {
if (!fu_engine_emulator_load_phases(self, archive, write_cnt, &got_json, error))
return FALSE;
}
if (!got_json) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"no emulation data found in archive");
return FALSE;
}
/* success */
return TRUE;
}
static void
fu_engine_emulator_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
FuEngineEmulator *self = FU_ENGINE_EMULATOR(object);
switch (prop_id) {
case PROP_ENGINE:
g_value_set_object(value, self->engine);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
static void
fu_engine_emulator_set_property(GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
FuEngineEmulator *self = FU_ENGINE_EMULATOR(object);
switch (prop_id) {
case PROP_ENGINE:
self->engine = g_value_dup_object(value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
static void
fu_engine_emulator_init(FuEngineEmulator *self)
{
self->phase_blobs =
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_bytes_unref);
}
static void
fu_engine_emulator_finalize(GObject *obj)
{
FuEngineEmulator *self = FU_ENGINE_EMULATOR(obj);
g_hash_table_unref(self->phase_blobs);
G_OBJECT_CLASS(fu_engine_emulator_parent_class)->finalize(obj);
}
static void
fu_engine_emulator_dispose(GObject *obj)
{
FuEngineEmulator *self = FU_ENGINE_EMULATOR(obj);
g_clear_object(&self->engine);
G_OBJECT_CLASS(fu_engine_emulator_parent_class)->dispose(obj);
}
static void
fu_engine_emulator_class_init(FuEngineEmulatorClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
GParamSpec *pspec;
object_class->finalize = fu_engine_emulator_finalize;
object_class->dispose = fu_engine_emulator_dispose;
object_class->get_property = fu_engine_emulator_get_property;
object_class->set_property = fu_engine_emulator_set_property;
pspec =
g_param_spec_object("engine",
NULL,
NULL,
FU_TYPE_ENGINE,
G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
g_object_class_install_property(object_class, PROP_ENGINE, pspec);
}
FuEngineEmulator *
fu_engine_emulator_new(FuEngine *engine)
{
return FU_ENGINE_EMULATOR(g_object_new(FU_TYPE_ENGINE_EMULATOR, "engine", engine, NULL));
}