/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2015 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */

/**
 * SECTION:dfu-firmware
 * @short_description: Object representing a DFU or DfuSe firmware file
 *
 * This object allows reading and writing firmware files either in
 * raw, DFU or DfuSe formats.
 *
 * A #DfuFirmware can be made up of several #DfuImages, although
 * typically there is only one.
 *
 * See also: #DfuImage
 */

#include "config.h"

#include <string.h>
#include <stdio.h>
#include <appstream-glib.h>

#include "dfu-common.h"
#include "dfu-error.h"
#include "dfu-firmware-private.h"
#include "dfu-format-elf.h"
#include "dfu-format-dfu.h"
#include "dfu-format-ihex.h"
#include "dfu-format-raw.h"
#include "dfu-image.h"

static void dfu_firmware_finalize			 (GObject *object);

typedef struct {
	GHashTable		*metadata;
	GHashTable		*symtab;
	GPtrArray		*images;
	guint16			 vid;
	guint16			 pid;
	guint16			 release;
	DfuCipherKind		 cipher_kind;
	DfuFirmwareFormat	 format;
} DfuFirmwarePrivate;

G_DEFINE_TYPE_WITH_PRIVATE (DfuFirmware, dfu_firmware, G_TYPE_OBJECT)
#define GET_PRIVATE(o) (dfu_firmware_get_instance_private (o))

static void
dfu_firmware_class_init (DfuFirmwareClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = dfu_firmware_finalize;
}

static void
dfu_firmware_init (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	priv->vid = 0xffff;
	priv->pid = 0xffff;
	priv->release = 0xffff;
	priv->images = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->metadata = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	priv->symtab = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
}

static void
dfu_firmware_finalize (GObject *object)
{
	DfuFirmware *firmware = DFU_FIRMWARE (object);
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);

	g_ptr_array_unref (priv->images);
	g_hash_table_destroy (priv->metadata);
	g_hash_table_destroy (priv->symtab);

	G_OBJECT_CLASS (dfu_firmware_parent_class)->finalize (object);
}

/**
 * dfu_firmware_new:
 *
 * Creates a new DFU firmware object.
 *
 * Return value: a new #DfuFirmware
 *
 * Since: 0.5.4
 **/
DfuFirmware *
dfu_firmware_new (void)
{
	DfuFirmware *firmware;
	firmware = g_object_new (DFU_TYPE_FIRMWARE, NULL);
	return firmware;
}

/**
 * dfu_firmware_get_image:
 * @firmware: a #DfuFirmware
 * @alt_setting: an alternative setting, typically 0x00
 *
 * Gets an image from the firmware file.
 *
 * Return value: (transfer none): a #DfuImage, or %NULL for not found
 *
 * Since: 0.5.4
 **/
DfuImage *
dfu_firmware_get_image (DfuFirmware *firmware, guint8 alt_setting)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	DfuImage *im;
	guint i;

	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), NULL);

	/* find correct image */
	for (i = 0; i < priv->images->len; i++) {
		im = g_ptr_array_index (priv->images, i);
		if (dfu_image_get_alt_setting (im) == alt_setting)
			return im;
	}
	return NULL;
}

/**
 * dfu_firmware_get_image_by_name:
 * @firmware: a #DfuFirmware
 * @name: an alternative setting name
 *
 * Gets an image from the firmware file.
 *
 * Return value: (transfer none): a #DfuImage, or %NULL for not found
 *
 * Since: 0.5.4
 **/
DfuImage *
dfu_firmware_get_image_by_name (DfuFirmware *firmware, const gchar *name)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	DfuImage *im;
	guint i;

	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), NULL);

	/* find correct image */
	for (i = 0; i < priv->images->len; i++) {
		im = g_ptr_array_index (priv->images, i);
		if (g_strcmp0 (dfu_image_get_name (im), name) == 0)
			return im;
	}
	return NULL;
}

/**
 * dfu_firmware_get_image_default:
 * @firmware: a #DfuFirmware
 *
 * Gets the default image from the firmware file.
 *
 * Return value: (transfer none): a #DfuImage, or %NULL for not found
 *
 * Since: 0.5.4
 **/
DfuImage *
dfu_firmware_get_image_default (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), NULL);
	if (priv->images->len == 0)
		return NULL;
	return g_ptr_array_index (priv->images, 0);
}

/**
 * dfu_firmware_get_images:
 * @firmware: a #DfuFirmware
 *
 * Gets all the images contained in this firmware file.
 *
 * Return value: (transfer none) (element-type DfuImage): list of images
 *
 * Since: 0.5.4
 **/
GPtrArray *
dfu_firmware_get_images (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), NULL);
	return priv->images;
}

/**
 * dfu_firmware_get_size:
 * @firmware: a #DfuFirmware
 *
 * Gets the size of all the images in the firmware.
 *
 * This only returns actual data that would be sent to the device and
 * does not include any padding.
 *
 * Return value: a integer value, or 0 if there are no images.
 *
 * Since: 0.5.4
 **/
guint32
dfu_firmware_get_size (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	guint32 length = 0;
	guint i;
	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), 0);
	for (i = 0; i < priv->images->len; i++) {
		DfuImage *image = g_ptr_array_index (priv->images, i);
		length += dfu_image_get_size (image);
	}
	return length;
}

/**
 * dfu_firmware_add_image:
 * @firmware: a #DfuFirmware
 * @image: a #DfuImage
 *
 * Adds an image to the list of images.
 *
 * Since: 0.5.4
 **/
void
dfu_firmware_add_image (DfuFirmware *firmware, DfuImage *image)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_if_fail (DFU_IS_FIRMWARE (firmware));
	g_return_if_fail (DFU_IS_IMAGE (image));
	g_ptr_array_add (priv->images, g_object_ref (image));
}

/**
 * dfu_firmware_get_vid:
 * @firmware: a #DfuFirmware
 *
 * Gets the vendor ID.
 *
 * Return value: a vendor ID, or 0xffff for unset
 *
 * Since: 0.5.4
 **/
guint16
dfu_firmware_get_vid (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), 0xffff);
	return priv->vid;
}

/**
 * dfu_firmware_get_pid:
 * @firmware: a #DfuFirmware
 *
 * Gets the product ID.
 *
 * Return value: a product ID, or 0xffff for unset
 *
 * Since: 0.5.4
 **/
guint16
dfu_firmware_get_pid (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), 0xffff);
	return priv->pid;
}

/**
 * dfu_firmware_get_release:
 * @firmware: a #DfuFirmware
 *
 * Gets the device ID.
 *
 * Return value: a device ID, or 0xffff for unset
 *
 * Since: 0.5.4
 **/
guint16
dfu_firmware_get_release (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), 0xffff);
	return priv->release;
}

/**
 * dfu_firmware_get_format:
 * @firmware: a #DfuFirmware
 *
 * Gets the DFU version.
 *
 * Return value: a version, or 0x0 for unset
 *
 * Since: 0.5.4
 **/
guint16
dfu_firmware_get_format (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), 0xffff);
	return priv->format;
}

/**
 * dfu_firmware_set_vid:
 * @firmware: a #DfuFirmware
 * @vid: vendor ID, or 0xffff for unset
 *
 * Sets the vendor ID.
 *
 * Since: 0.5.4
 **/
void
dfu_firmware_set_vid (DfuFirmware *firmware, guint16 vid)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_if_fail (DFU_IS_FIRMWARE (firmware));
	priv->vid = vid;
}

/**
 * dfu_firmware_set_pid:
 * @firmware: a #DfuFirmware
 * @pid: product ID, or 0xffff for unset
 *
 * Sets the product ID.
 *
 * Since: 0.5.4
 **/
void
dfu_firmware_set_pid (DfuFirmware *firmware, guint16 pid)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_if_fail (DFU_IS_FIRMWARE (firmware));
	priv->pid = pid;
}

/**
 * dfu_firmware_set_release:
 * @firmware: a #DfuFirmware
 * @release: device ID, or 0xffff for unset
 *
 * Sets the device ID.
 *
 * Since: 0.5.4
 **/
void
dfu_firmware_set_release (DfuFirmware *firmware, guint16 release)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_if_fail (DFU_IS_FIRMWARE (firmware));
	priv->release = release;
}

/**
 * dfu_firmware_set_format:
 * @firmware: a #DfuFirmware
 * @format: a #DfuFirmwareFormat, e.g. %DFU_FIRMWARE_FORMAT_DFUSE
 *
 * Sets the DFU version in BCD format.
 *
 * Since: 0.5.4
 **/
void
dfu_firmware_set_format (DfuFirmware *firmware, DfuFirmwareFormat format)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_if_fail (DFU_IS_FIRMWARE (firmware));
	priv->format = format;
}

static void
dfu_firmware_parse_altos_vid_pid (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	guint32 addr32;

	/* find symbol */
	addr32 = dfu_firmware_lookup_symbol (firmware, "ao_usb_descriptors");
	if (addr32 == 0x0)
		return;

	/* search each element */
	for (guint i = 0; i < priv->images->len; i++) {
		DfuImage *image = g_ptr_array_index (priv->images, i);
		DfuElement *element;
		GBytes *bytes_tmp;
		const guint8 *data;
		gsize length;
		guint32 element_address;
		guint16 vid, pid, release;

		/* get element data */
		element = dfu_image_get_element_default (image);
		if (element == NULL)
			continue;

		/* check address is in this element */
		element_address = dfu_element_get_address (element);
		if (element_address > addr32)
			continue;
		bytes_tmp = dfu_element_get_contents (element);
		if (bytes_tmp == NULL)
			continue;

		/* check this element is big enough */
		data = g_bytes_get_data (bytes_tmp, &length);
		if (addr32 - element_address > length)
			continue;

		/* read the USB descriptor */
		addr32 -= element_address;
		memcpy (&vid, &data[addr32 + 8], 2);
		memcpy (&pid, &data[addr32 + 10], 2);
		memcpy (&release, &data[addr32 + 12], 2);
		dfu_firmware_set_vid (firmware, GUINT32_FROM_LE (vid));
		dfu_firmware_set_pid (firmware, GUINT32_FROM_LE (pid));
		dfu_firmware_set_release (firmware, GUINT32_FROM_LE (release));
	}
}

/**
 * dfu_firmware_parse_data:
 * @firmware: a #DfuFirmware
 * @bytes: raw firmware data
 * @flags: optional flags, e.g. %DFU_FIRMWARE_PARSE_FLAG_NO_CRC_TEST
 * @error: a #GError, or %NULL
 *
 * Parses firmware data which may have an optional DFU suffix.
 *
 * Return value: %TRUE for success
 *
 * Since: 0.5.4
 **/
gboolean
dfu_firmware_parse_data (DfuFirmware *firmware, GBytes *bytes,
			 DfuFirmwareParseFlags flags, GError **error)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);

	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), FALSE);
	g_return_val_if_fail (bytes != NULL, FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* set defaults */
	priv->vid = 0xffff;
	priv->pid = 0xffff;
	priv->release = 0xffff;

	/* try to get format if not already set */
	if (priv->format == DFU_FIRMWARE_FORMAT_UNKNOWN)
		priv->format = dfu_firmware_detect_ihex (bytes);
	if (priv->format == DFU_FIRMWARE_FORMAT_UNKNOWN)
		priv->format = dfu_firmware_detect_dfu (bytes);
	if (priv->format == DFU_FIRMWARE_FORMAT_UNKNOWN)
		priv->format = dfu_firmware_detect_elf (bytes);
	if (priv->format == DFU_FIRMWARE_FORMAT_UNKNOWN)
		priv->format = dfu_firmware_detect_raw (bytes);

	/* handled easily */
	switch (priv->format) {
	case DFU_FIRMWARE_FORMAT_INTEL_HEX:
		if (!dfu_firmware_from_ihex (firmware, bytes, flags, error))
			return FALSE;
		break;
	case DFU_FIRMWARE_FORMAT_ELF:
		if (!dfu_firmware_from_elf (firmware, bytes, flags, error))
			return FALSE;
		break;
	case DFU_FIRMWARE_FORMAT_DFU:
	case DFU_FIRMWARE_FORMAT_DFUSE:
		if (!dfu_firmware_from_dfu (firmware, bytes, flags, error))
			return FALSE;
		break;
	default:
		if (!dfu_firmware_from_raw (firmware, bytes, flags, error))
			return FALSE;
		break;
	}

	/* get the VID/PID for altos devices */
	dfu_firmware_parse_altos_vid_pid (firmware);

	return TRUE;
}

/**
 * dfu_firmware_parse_file:
 * @firmware: a #DfuFirmware
 * @file: a #GFile to load and parse
 * @flags: optional flags, e.g. %DFU_FIRMWARE_PARSE_FLAG_NO_CRC_TEST
 * @cancellable: a #GCancellable, or %NULL
 * @error: a #GError, or %NULL
 *
 * Parses a DFU firmware, which may contain an optional footer.
 *
 * Return value: %TRUE for success
 *
 * Since: 0.5.4
 **/
gboolean
dfu_firmware_parse_file (DfuFirmware *firmware, GFile *file,
			 DfuFirmwareParseFlags flags,
			 GCancellable *cancellable, GError **error)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	gchar *contents = NULL;
	gsize length = 0;
	g_autofree gchar *basename = NULL;
	g_autoptr(GBytes) bytes = NULL;

	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), FALSE);
	g_return_val_if_fail (G_IS_FILE (file), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* guess cipher kind based on file extension */
	basename = g_file_get_basename (file);
	if (g_str_has_suffix (basename, ".xdfu"))
		priv->cipher_kind = DFU_CIPHER_KIND_XTEA;

	if (!g_file_load_contents (file, cancellable, &contents,
				   &length, NULL, error))
		return FALSE;
	bytes = g_bytes_new_take (contents, length);
	return dfu_firmware_parse_data (firmware, bytes, flags, error);
}

/**
 * dfu_firmware_get_metadata:
 * @firmware: a #DfuFirmware
 * @key: metadata string key
 *
 * Gets metadata from the store with a specific key.
 *
 * Return value: the metadata value, or %NULL for unset
 *
 * Since: 0.5.4
 **/
const gchar *
dfu_firmware_get_metadata (DfuFirmware *firmware, const gchar *key)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	return g_hash_table_lookup (priv->metadata, key);
}

/**
 * dfu_firmware_get_metadata_table:
 * @firmware: a #DfuFirmware
 *
 * Gets all metadata from the store.
 *
 * Return value: (transfer none): the metadata hash table
 *
 * Since: 0.6.3
 **/
GHashTable *
dfu_firmware_get_metadata_table (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	return priv->metadata;
}

/**
 * dfu_firmware_set_metadata:
 * @firmware: a #DfuFirmware
 * @key: metadata string key
 * @value: metadata string value
 *
 * Sets a metadata value with a specific key.
 *
 * Since: 0.5.4
 **/
void
dfu_firmware_set_metadata (DfuFirmware *firmware, const gchar *key, const gchar *value)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_debug ("adding metadata %s=%s", key, value);
	g_hash_table_insert (priv->metadata, g_strdup (key), g_strdup (value));
}

/**
 * dfu_firmware_remove_metadata:
 * @firmware: a #DfuFirmware
 * @key: metadata string key
 *
 * Removes a metadata item from the store
 *
 * Since: 0.5.4
 **/
void
dfu_firmware_remove_metadata (DfuFirmware *firmware, const gchar *key)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_debug ("removing metadata %s", key);
	g_hash_table_remove (priv->metadata, key);
}

/**
 * dfu_firmware_write_data:
 * @firmware: a #DfuFirmware
 * @error: a #GError, or %NULL
 *
 * Writes DFU data to a data blob with a DFU-specific footer.
 *
 * Return value: (transfer none): firmware data
 *
 * Since: 0.5.4
 **/
GBytes *
dfu_firmware_write_data (DfuFirmware *firmware, GError **error)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);

	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	/* at least one image */
	if (priv->images == 0) {
		g_set_error_literal (error,
				     DFU_ERROR,
				     DFU_ERROR_INTERNAL,
				     "no image data to write");
		return NULL;
	}

	/* DFU only supports one image */
	if (priv->images->len > 1 &&
	    priv->format != DFU_FIRMWARE_FORMAT_DFUSE) {
		g_set_error (error,
			     DFU_ERROR,
			     DFU_ERROR_INTERNAL,
			     "only DfuSe format supports multiple images (%u)",
			     priv->images->len);
		return NULL;
	}

	/* raw */
	if (priv->format == DFU_FIRMWARE_FORMAT_RAW)
		return dfu_firmware_to_raw (firmware, error);

	/* DFU or DfuSe*/
	if (priv->format == DFU_FIRMWARE_FORMAT_DFU ||
	    priv->format == DFU_FIRMWARE_FORMAT_DFUSE)
		return dfu_firmware_to_dfu (firmware, error);

	/* Intel HEX */
	if (priv->format == DFU_FIRMWARE_FORMAT_INTEL_HEX)
		return dfu_firmware_to_ihex (firmware, error);

	/* ELF */
	if (priv->format == DFU_FIRMWARE_FORMAT_ELF)
		return dfu_firmware_to_elf (firmware, error);

	/* invalid */
	g_set_error (error,
		     DFU_ERROR,
		     DFU_ERROR_INTERNAL,
		     "invalid format for write (0x%04x)",
		     priv->format);
	return NULL;
}

/**
 * dfu_firmware_write_file:
 * @firmware: a #DfuFirmware
 * @file: a #GFile
 * @cancellable: a #GCancellable, or %NULL
 * @error: a #GError, or %NULL
 *
 * Writes a DFU firmware with the optional footer.
 *
 * Return value: %TRUE for success
 *
 * Since: 0.5.4
 **/
gboolean
dfu_firmware_write_file (DfuFirmware *firmware, GFile *file,
			 GCancellable *cancellable, GError **error)
{
	const guint8 *data;
	gsize length = 0;
	g_autoptr(GBytes) bytes = NULL;

	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), FALSE);
	g_return_val_if_fail (G_IS_FILE (file), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* get blob */
	bytes = dfu_firmware_write_data (firmware, error);
	if (bytes == NULL)
		return FALSE;

	/* save to firmware */
	data = g_bytes_get_data (bytes, &length);
	return g_file_replace_contents (file,
					(const gchar *) data,
					length,
					NULL,
					FALSE,
					G_FILE_CREATE_NONE,
					NULL,
					cancellable,
					error);
}

/**
 * dfu_firmware_to_string:
 * @firmware: a #DfuFirmware
 *
 * Returns a string representaiton of the object.
 *
 * Return value: NULL terminated string, or %NULL for invalid
 *
 * Since: 0.5.4
 **/
gchar *
dfu_firmware_to_string (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	DfuImage *image;
	GList *l;
	GString *str;
	guint i;
	g_autofree gchar *release_str = NULL;
	g_autoptr(GList) keys = NULL;

	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), NULL);

	release_str = as_utils_version_from_uint16 (priv->release,
						    AS_VERSION_PARSE_FLAG_NONE);
	str = g_string_new ("");
	g_string_append_printf (str, "vid:         0x%04x\n", priv->vid);
	g_string_append_printf (str, "pid:         0x%04x\n", priv->pid);
	g_string_append_printf (str, "release:     0x%04x [%s]\n",
				priv->release, release_str);
	g_string_append_printf (str, "format:      %s [0x%04x]\n",
				dfu_firmware_format_to_string (priv->format),
				priv->format);
	g_string_append_printf (str, "cipher:      %s\n",
				dfu_cipher_kind_to_string (priv->cipher_kind));

	/* print metadata */
	keys = g_hash_table_get_keys (priv->metadata);
	for (l = keys; l != NULL; l = l->next) {
		const gchar *key;
		const gchar *value;
		key = l->data;
		value = g_hash_table_lookup (priv->metadata, key);
		g_string_append_printf (str, "metadata:    %s=%s\n", key, value);
	}

	/* print images */
	for (i = 0; i < priv->images->len; i++) {
		g_autofree gchar *tmp = NULL;
		image = g_ptr_array_index (priv->images, i);
		tmp = dfu_image_to_string (image);
		g_string_append_printf (str, "= IMAGE %u =\n", i);
		g_string_append_printf (str, "%s\n", tmp);
	}

	g_string_truncate (str, str->len - 1);
	return g_string_free (str, FALSE);
}

/**
 * dfu_firmware_format_to_string:
 * @format: a #DfuFirmwareFormat, e.g. %DFU_FIRMWARE_FORMAT_DFU
 *
 * Returns a string representaiton of the format.
 *
 * Return value: NULL terminated string, or %NULL for invalid
 *
 * Since: 0.5.4
 **/
const gchar *
dfu_firmware_format_to_string (DfuFirmwareFormat format)
{
	if (format == DFU_FIRMWARE_FORMAT_RAW)
		return "raw";
	if (format == DFU_FIRMWARE_FORMAT_DFU)
		return "dfu";
	if (format == DFU_FIRMWARE_FORMAT_DFUSE)
		return "dfuse";
	if (format == DFU_FIRMWARE_FORMAT_INTEL_HEX)
		return "ihex";
	if (format == DFU_FIRMWARE_FORMAT_ELF)
		return "elf";
	return NULL;
}

/**
 * dfu_firmware_format_from_string:
 * @format: a format string, e.g. `dfuse`
 *
 * Returns an enumerated version of the format.
 *
 * Return value: a #DfuFirmwareFormat, e.g. %DFU_FIRMWARE_FORMAT_DFUSE
 *
 * Since: 0.7.3
 **/
DfuFirmwareFormat
dfu_firmware_format_from_string (const gchar *format)
{
	if (g_strcmp0 (format, "raw") == 0)
		return DFU_FIRMWARE_FORMAT_RAW;
	if (g_strcmp0 (format, "dfu") == 0)
		return DFU_FIRMWARE_FORMAT_DFU;
	if (g_strcmp0 (format, "dfuse") == 0)
		return DFU_FIRMWARE_FORMAT_DFUSE;
	if (g_strcmp0 (format, "ihex") == 0)
		return DFU_FIRMWARE_FORMAT_INTEL_HEX;
	if (g_strcmp0 (format, "elf") == 0)
		return DFU_FIRMWARE_FORMAT_ELF;
	return DFU_FIRMWARE_FORMAT_UNKNOWN;
}

/**
 * dfu_firmware_get_cipher_kind:
 * @firmware: a #DfuFirmware
 *
 * Returns the kind of cipher used by the firmware file.
 *
 * NOTE: this value is based on a heuristic, and may not be accurate.
 * The value %DFU_CIPHER_KIND_NONE will be returned when the cipher
 * is not recognised.
 *
 * Return value: NULL terminated string, or %NULL for invalid
 *
 * Since: 0.5.4
 **/
DfuCipherKind
dfu_firmware_get_cipher_kind (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_val_if_fail (DFU_IS_FIRMWARE (firmware), 0);
	return priv->cipher_kind;
}

/**
 * dfu_firmware_set_cipher_kind:
 * @firmware: a #DfuFirmware
 * @cipher_kind: a #DfuCipherKind, e.g. %DFU_CIPHER_KIND_XTEA
 *
 * Sets the kind of cipher used by the firmware file.
 *
 * Since: 0.6.3
 **/
void
dfu_firmware_set_cipher_kind (DfuFirmware *firmware, DfuCipherKind cipher_kind)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_return_if_fail (DFU_IS_FIRMWARE (firmware));
	priv->cipher_kind = cipher_kind;
}

/**
 * dfu_firmware_add_symbol:
 * @firmware: a #DfuFirmware
 * @symbol_name: a valid symbol name
 * @symbol_addr: a symbol memory address
 *
 * Adds a symbol to the global map for the firmware.
 *
 * NOTE: Only certain types of firmware can contain a symbol table.
 *
 * Since: 0.7.4
 **/
void
dfu_firmware_add_symbol (DfuFirmware *firmware,
			 const gchar *symbol_name,
			 guint32 symbol_addr)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	g_hash_table_insert (priv->symtab,
			     g_strdup (symbol_name),
			     GUINT_TO_POINTER (symbol_addr));
}

/**
 * dfu_firmware_lookup_symbol:
 * @firmware: a #DfuFirmware
 * @symbol_name: a valid symbol name
 *
 * Returns the address of a symbol from the global map.
 *
 * NOTE: Only certain types of firmware can contain a symbol table.
 *
 * Return value: address of the symbol, or 0x0 for not found.
 *
 * Since: 0.7.4
 **/
guint32
dfu_firmware_lookup_symbol (DfuFirmware *firmware, const gchar *symbol_name)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	return GPOINTER_TO_UINT (g_hash_table_lookup (priv->symtab,
						      symbol_name));
}

/**
 * dfu_firmware_get_symbols:
 * @firmware: a #DfuFirmware
 *
 * Gets all the symbols currently defined.
 *
 * Return value: (element-type utf8) (transfer container): symbol names
 *
 * Since: 0.7.4
 **/
GPtrArray *
dfu_firmware_get_symbols (DfuFirmware *firmware)
{
	DfuFirmwarePrivate *priv = GET_PRIVATE (firmware);
	GPtrArray *array = g_ptr_array_new_with_free_func (g_free);
	GList *l;
	g_autoptr(GList) keys = g_hash_table_get_keys (priv->symtab);
	for (l = keys; l != NULL; l = l->next)
		g_ptr_array_add (array, g_strdup (l->data));
	return array;
}
