/*
 * Copyright (C) 2021 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2022 Gaël PORTAY <gael.portay@collabora.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#define G_LOG_DOMAIN "FuFirmware"

#include "config.h"

#include "fu-archive-firmware.h"
#include "fu-archive.h"
#include "fu-common.h"
#include "fu-path.h"

/**
 * FuArchiveFirmware:
 *
 * An archive firmware image, typically for nested firmware volumes.
 *
 * See also: [class@FuFirmware]
 */

typedef struct {
	FuArchiveFormat format;
	FuArchiveCompression compression;
} FuArchiveFirmwarePrivate;

G_DEFINE_TYPE_WITH_PRIVATE(FuArchiveFirmware, fu_archive_firmware, FU_TYPE_FIRMWARE)
#define GET_PRIVATE(o) (fu_archive_firmware_get_instance_private(o))

static void
fu_archive_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn)
{
	FuArchiveFirmware *self = FU_ARCHIVE_FIRMWARE(firmware);
	FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self);
	fu_xmlb_builder_insert_kv(bn, "format", fu_archive_format_to_string(priv->format));
	fu_xmlb_builder_insert_kv(bn,
				  "compression",
				  fu_archive_compression_to_string(priv->compression));
}

static gboolean
fu_archive_firmware_parse_cb(FuArchive *self,
			     const gchar *filename,
			     GBytes *bytes,
			     gpointer user_data,
			     GError **error)
{
	FuFirmware *firmware = FU_FIRMWARE(user_data);
	g_autoptr(FuFirmware) img = fu_firmware_new_from_bytes(bytes);
	fu_firmware_set_id(img, filename);
	fu_firmware_add_image(firmware, img);
	return TRUE;
}

static gboolean
fu_archive_firmware_parse(FuFirmware *firmware,
			  GBytes *fw,
			  gsize offset,
			  FwupdInstallFlags flags,
			  GError **error)
{
	g_autoptr(FuArchive) archive = NULL;

	/* load archive */
	archive = fu_archive_new(fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error);
	if (archive == NULL)
		return FALSE;

	/* decompress each image in the archive */
	return fu_archive_iterate(archive, fu_archive_firmware_parse_cb, firmware, error);
}

/**
 * fu_archive_firmware_get_format:
 * @self: a #FuArchiveFirmware
 *
 * Gets the archive format.
 *
 * Returns: format
 *
 * Since: 1.8.1
 **/
FuArchiveFormat
fu_archive_firmware_get_format(FuArchiveFirmware *self)
{
	FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_ARCHIVE_FIRMWARE(self), FU_ARCHIVE_FORMAT_UNKNOWN);
	return priv->format;
}

/**
 * fu_archive_firmware_set_format:
 * @self: a #FuArchiveFirmware
 * @format: the archive format, e.g. %FU_ARCHIVE_FORMAT_ZIP
 *
 * Sets the archive format.
 *
 * Since: 1.8.1
 **/
void
fu_archive_firmware_set_format(FuArchiveFirmware *self, FuArchiveFormat format)
{
	FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self);
	g_return_if_fail(FU_IS_ARCHIVE_FIRMWARE(self));
	priv->format = format;
}

/**
 * fu_archive_firmware_get_compression:
 * @self: A #FuArchiveFirmware
 *
 * Returns the compression.
 *
 * Returns: compression
 *
 * Since: 1.8.1
 **/
FuArchiveCompression
fu_archive_firmware_get_compression(FuArchiveFirmware *self)
{
	FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_ARCHIVE_FIRMWARE(self), FU_ARCHIVE_COMPRESSION_UNKNOWN);
	return priv->compression;
}

/**
 * fu_archive_firmware_set_compression:
 * @self: A #FuArchiveFirmware
 * @compression: the compression, e.g. %FU_ARCHIVE_COMPRESSION_NONE
 *
 * Sets the compression.
 *
 * Since: 1.8.1
 **/
void
fu_archive_firmware_set_compression(FuArchiveFirmware *self, FuArchiveCompression compression)
{
	FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self);
	g_return_if_fail(FU_IS_ARCHIVE_FIRMWARE(self));
	priv->compression = compression;
}

/**
 * fu_archive_firmware_get_image_fnmatch:
 * @self: a #FuPlugin
 * @pattern: (not nullable): a glob pattern, e.g. `*foo*`
 * @error: (nullable): optional return location for an error
 *
 * Gets a single firmware image using the image ID pattern. It is also an error for multiple images
 * to match.
 *
 * Returns: (transfer full): a #FuFirmware, or %NULL if the image is not found
 *
 * Since: 1.8.9
 **/
FuFirmware *
fu_archive_firmware_get_image_fnmatch(FuArchiveFirmware *self, const gchar *pattern, GError **error)
{
	g_autoptr(FuFirmware) img_match = NULL;
	g_autoptr(GPtrArray) imgs = fu_firmware_get_images(FU_FIRMWARE(self));

	g_return_val_if_fail(FU_IS_ARCHIVE_FIRMWARE(self), NULL);
	g_return_val_if_fail(pattern != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	for (guint i = 0; i < imgs->len; i++) {
		FuFirmware *img = g_ptr_array_index(imgs, i);
		const gchar *fn = fu_firmware_get_id(img);
		if (!fu_path_fnmatch(pattern, fn))
			continue;
		if (img_match != NULL) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_ARGUMENT,
				    "multiple images matched %s",
				    pattern);
			return NULL;
		}
		img_match = g_object_ref(img);
	}
	if (img_match == NULL) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_FOUND,
			    "no image matched %s",
			    pattern);
		return NULL;
	}
	return g_steal_pointer(&img_match);
}

static GBytes *
fu_archive_firmware_write(FuFirmware *firmware, GError **error)
{
	FuArchiveFirmware *self = FU_ARCHIVE_FIRMWARE(firmware);
	FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self);
	g_autoptr(FuArchive) archive = NULL;
	g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware);

	/* sanity check */
	if (priv->format == FU_ARCHIVE_FORMAT_UNKNOWN) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "firmware archive format unspecified");
		return NULL;
	}
	if (priv->compression == FU_ARCHIVE_COMPRESSION_UNKNOWN) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "firmware archive compression unspecified");
		return NULL;
	}

	/* save archive and compress each image to the archive */
	archive = fu_archive_new(NULL, FU_ARCHIVE_FLAG_NONE, NULL);
	for (guint i = 0; i < images->len; i++) {
		FuFirmware *img = g_ptr_array_index(images, i);
		g_autoptr(GBytes) blob = NULL;

		if (fu_firmware_get_id(img) == NULL) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "image has no ID");
			return NULL;
		}
		blob = fu_firmware_get_bytes(img, error);
		if (blob == NULL)
			return NULL;
		fu_archive_add_entry(archive, fu_firmware_get_id(img), blob);
	}
	return fu_archive_write(archive, priv->format, priv->compression, error);
}

static gboolean
fu_archive_firmware_build(FuFirmware *firmware, XbNode *n, GError **error)
{
	FuArchiveFirmware *self = FU_ARCHIVE_FIRMWARE(firmware);
	const gchar *tmp;

	/* simple properties */
	tmp = xb_node_query_text(n, "format", NULL);
	if (tmp != NULL) {
		FuArchiveFormat format = fu_archive_format_from_string(tmp);
		if (format == FU_ARCHIVE_FORMAT_UNKNOWN) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "format %s not supported",
				    tmp);
			return FALSE;
		}
		fu_archive_firmware_set_format(self, format);
	}
	tmp = xb_node_query_text(n, "compression", NULL);
	if (tmp != NULL) {
		FuArchiveCompression compression = fu_archive_compression_from_string(tmp);
		if (compression == FU_ARCHIVE_COMPRESSION_UNKNOWN) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "compression %s not supported",
				    tmp);
			return FALSE;
		}
		fu_archive_firmware_set_compression(self, compression);
	}

	/* success */
	return TRUE;
}

static void
fu_archive_firmware_init(FuArchiveFirmware *self)
{
}

static void
fu_archive_firmware_class_init(FuArchiveFirmwareClass *klass)
{
	FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
	klass_firmware->parse = fu_archive_firmware_parse;
	klass_firmware->write = fu_archive_firmware_write;
	klass_firmware->build = fu_archive_firmware_build;
	klass_firmware->export = fu_archive_firmware_export;
}

/**
 * fu_archive_firmware_new:
 *
 * Creates a new archive #FuFirmware
 *
 * Since: 1.7.3
 **/
FuFirmware *
fu_archive_firmware_new(void)
{
	return FU_FIRMWARE(g_object_new(FU_TYPE_ARCHIVE_FIRMWARE, NULL));
}
