/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2014 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:as-app
 * @short_description: An object for an AppStream application or add-on
 * @include: appstream-glib.h
 * @stability: Stable
 *
 * This object represents the base object of all AppStream, the application.
 * Although called #AsApp, this object also represents components like fonts,
 * codecs and input methods.
 *
 * See also: #AsScreenshot, #AsRelease
 */

#include "config.h"

#include <fnmatch.h>
#include <string.h>

#include "as-app-private.h"
#include "as-cleanup.h"
#include "as-enums.h"
#include "as-node-private.h"
#include "as-provide-private.h"
#include "as-release-private.h"
#include "as-screenshot-private.h"
#include "as-tag.h"
#include "as-utils-private.h"
#include "as-yaml.h"

typedef struct _AsAppPrivate	AsAppPrivate;
struct _AsAppPrivate
{
	AsAppProblems	 problems;
	AsIconKind	 icon_kind;
	AsIdKind	 id_kind;
	GHashTable	*comments;			/* of locale:string */
	GHashTable	*developer_names;		/* of locale:string */
	GHashTable	*descriptions;			/* of locale:string */
	GHashTable	*keywords;			/* of locale:GPtrArray */
	GHashTable	*languages;			/* of locale:string */
	GHashTable	*metadata;			/* of key:value */
	GHashTable	*names;				/* of locale:string */
	GHashTable	*urls;				/* of key:string */
	GPtrArray	*addons;			/* of AsApp */
	GPtrArray	*categories;			/* of string */
	GPtrArray	*compulsory_for_desktops;	/* of string */
	GPtrArray	*extends;			/* of string */
	GPtrArray	*kudos;				/* of string */
	GPtrArray	*mimetypes;			/* of string */
	GPtrArray	*pkgnames;			/* of string */
	GPtrArray	*architectures;			/* of string */
	GPtrArray	*releases;			/* of AsRelease */
	GPtrArray	*provides;			/* of AsProvide */
	GPtrArray	*screenshots;			/* of AsScreenshot */
	GPtrArray	*vetos;				/* of string */
	AsAppSourceKind	 source_kind;
	AsAppState	 state;
	AsAppTrustFlags	 trust_flags;
	gchar		*icon;
	gchar		*icon_path;
	gchar		*id_filename;
	gchar		*id;
	gchar		*project_group;
	gchar		*project_license;
	gchar		*metadata_license;
	gchar		*source_pkgname;
	gchar		*update_contact;
	gchar		*source_file;
	gint		 priority;
	gsize		 token_cache_valid;
	GPtrArray	*token_cache;			/* of AsAppTokenItem */
};

G_DEFINE_TYPE_WITH_PRIVATE (AsApp, as_app, G_TYPE_OBJECT)

#define GET_PRIVATE(o) (as_app_get_instance_private (o))

typedef struct {
	gchar		**values_ascii;
	gchar		**values_utf8;
	guint		  score;
} AsAppTokenItem;

/**
 * as_app_error_quark:
 *
 * Return value: An error quark.
 *
 * Since: 0.1.2
 **/
GQuark
as_app_error_quark (void)
{
	static GQuark quark = 0;
	if (!quark)
		quark = g_quark_from_static_string ("AsAppError");
	return quark;
}

/**
 * as_app_source_kind_from_string:
 * @source_kind: a source kind string
 *
 * Converts the text representation to an enumerated value.
 *
 * Return value: A #AsAppSourceKind, e.g. %AS_APP_SOURCE_KIND_APPSTREAM.
 *
 * Since: 0.2.2
 **/
AsAppSourceKind
as_app_source_kind_from_string (const gchar *source_kind)
{
	if (g_strcmp0 (source_kind, "appstream") == 0)
		return AS_APP_SOURCE_KIND_APPSTREAM;
	if (g_strcmp0 (source_kind, "appdata") == 0)
		return AS_APP_SOURCE_KIND_APPDATA;
	if (g_strcmp0 (source_kind, "metainfo") == 0)
		return AS_APP_SOURCE_KIND_METAINFO;
	if (g_strcmp0 (source_kind, "desktop") == 0)
		return AS_APP_SOURCE_KIND_DESKTOP;
	return AS_APP_SOURCE_KIND_UNKNOWN;
}

/**
 * as_app_source_kind_to_string:
 * @source_kind: the #AsAppSourceKind.
 *
 * Converts the enumerated value to an text representation.
 *
 * Returns: string version of @source_kind, or %NULL for unknown
 *
 * Since: 0.2.2
 **/
const gchar *
as_app_source_kind_to_string (AsAppSourceKind source_kind)
{
	if (source_kind == AS_APP_SOURCE_KIND_APPSTREAM)
		return "appstream";
	if (source_kind == AS_APP_SOURCE_KIND_APPDATA)
		return "appdata";
	if (source_kind == AS_APP_SOURCE_KIND_METAINFO)
		return "metainfo";
	if (source_kind == AS_APP_SOURCE_KIND_DESKTOP)
		return "desktop";
	return NULL;
}

/**
 * as_app_state_to_string:
 * @state: the #AsAppState.
 *
 * Converts the enumerated value to an text representation.
 *
 * Returns: string version of @state, or %NULL for unknown
 *
 * Since: 0.2.2
 **/
const gchar *
as_app_state_to_string (AsAppState state)
{
	if (state == AS_APP_STATE_UNKNOWN)
		return "unknown";
	if (state == AS_APP_STATE_INSTALLED)
		return "installed";
	if (state == AS_APP_STATE_AVAILABLE)
		return "available";
	if (state == AS_APP_STATE_AVAILABLE_LOCAL)
		return "local";
	if (state == AS_APP_STATE_QUEUED_FOR_INSTALL)
		return "queued";
	if (state == AS_APP_STATE_INSTALLING)
		return "installing";
	if (state == AS_APP_STATE_REMOVING)
		return "removing";
	if (state == AS_APP_STATE_UPDATABLE)
		return "updatable";
	if (state == AS_APP_STATE_UNAVAILABLE)
		return "unavailable";
	return NULL;
}

/**
 * as_app_guess_source_kind:
 * @filename: a file name
 *
 * Guesses the source kind based from the filename.
 *
 * Return value: A #AsAppSourceKind, e.g. %AS_APP_SOURCE_KIND_APPSTREAM.
 *
 * Since: 0.1.8
 **/
AsAppSourceKind
as_app_guess_source_kind (const gchar *filename)
{
	if (g_str_has_suffix (filename, ".xml.gz"))
		return AS_APP_SOURCE_KIND_APPSTREAM;
	if (g_str_has_suffix (filename, ".yml.gz"))
		return AS_APP_SOURCE_KIND_APPSTREAM;
	if (g_str_has_suffix (filename, ".desktop"))
		return AS_APP_SOURCE_KIND_DESKTOP;
	if (g_str_has_suffix (filename, ".desktop.in"))
		return AS_APP_SOURCE_KIND_DESKTOP;
	if (g_str_has_suffix (filename, ".appdata.xml"))
		return AS_APP_SOURCE_KIND_APPDATA;
	if (g_str_has_suffix (filename, ".appdata.xml.in"))
		return AS_APP_SOURCE_KIND_APPDATA;
	if (g_str_has_suffix (filename, ".metainfo.xml"))
		return AS_APP_SOURCE_KIND_METAINFO;
	if (g_str_has_suffix (filename, ".metainfo.xml.in"))
		return AS_APP_SOURCE_KIND_METAINFO;
	return AS_APP_SOURCE_KIND_UNKNOWN;
}

/**
 * as_app_finalize:
 **/
static void
as_app_finalize (GObject *object)
{
	AsApp *app = AS_APP (object);
	AsAppPrivate *priv = GET_PRIVATE (app);

	g_free (priv->icon);
	g_free (priv->icon_path);
	g_free (priv->id_filename);
	g_free (priv->id);
	g_free (priv->project_group);
	g_free (priv->project_license);
	g_free (priv->metadata_license);
	g_free (priv->source_pkgname);
	g_free (priv->update_contact);
	g_free (priv->source_file);
	g_hash_table_unref (priv->comments);
	g_hash_table_unref (priv->developer_names);
	g_hash_table_unref (priv->descriptions);
	g_hash_table_unref (priv->keywords);
	g_hash_table_unref (priv->languages);
	g_hash_table_unref (priv->metadata);
	g_hash_table_unref (priv->names);
	g_hash_table_unref (priv->urls);
	g_ptr_array_unref (priv->addons);
	g_ptr_array_unref (priv->categories);
	g_ptr_array_unref (priv->compulsory_for_desktops);
	g_ptr_array_unref (priv->extends);
	g_ptr_array_unref (priv->kudos);
	g_ptr_array_unref (priv->mimetypes);
	g_ptr_array_unref (priv->pkgnames);
	g_ptr_array_unref (priv->architectures);
	g_ptr_array_unref (priv->releases);
	g_ptr_array_unref (priv->provides);
	g_ptr_array_unref (priv->screenshots);
	g_ptr_array_unref (priv->token_cache);
	g_ptr_array_unref (priv->vetos);

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

/**
 * as_app_token_item_free:
 **/
static void
as_app_token_item_free (AsAppTokenItem *token_item)
{
	g_strfreev (token_item->values_ascii);
	g_strfreev (token_item->values_utf8);
	g_slice_free (AsAppTokenItem, token_item);
}

/**
 * as_app_init:
 **/
static void
as_app_init (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	priv->categories = g_ptr_array_new_with_free_func (g_free);
	priv->compulsory_for_desktops = g_ptr_array_new_with_free_func (g_free);
	priv->extends = g_ptr_array_new_with_free_func (g_free);
	priv->keywords = g_hash_table_new_full (g_str_hash, g_str_equal,
						g_free, (GDestroyNotify) g_ptr_array_unref);
	priv->kudos = g_ptr_array_new_with_free_func (g_free);
	priv->mimetypes = g_ptr_array_new_with_free_func (g_free);
	priv->pkgnames = g_ptr_array_new_with_free_func (g_free);
	priv->architectures = g_ptr_array_new_with_free_func (g_free);
	priv->addons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->releases = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->provides = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->screenshots = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->token_cache = g_ptr_array_new_with_free_func ((GDestroyNotify) as_app_token_item_free);
	priv->vetos = g_ptr_array_new_with_free_func (g_free);

	priv->comments = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	priv->developer_names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	priv->descriptions = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	priv->languages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	priv->metadata = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	priv->names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	priv->urls = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
}

/**
 * as_app_class_init:
 **/
static void
as_app_class_init (AsAppClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = as_app_finalize;
}

/******************************************************************************/

/**
 * as_app_get_id:
 * @app: a #AsApp instance.
 *
 * Gets the full ID value.
 *
 * Returns: the ID, e.g. "org.gnome.Software.desktop"
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_id (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->id;
}

/**
 * as_app_get_id_filename:
 * @app: a #AsApp instance.
 *
 * Returns a filename which represents the applications ID, e.g. "gimp.desktop"
 * becomes "gimp" and is used for cache directories.
 *
 * Returns: A utf8 filename
 *
 * Since: 0.3.0
 **/
const gchar *
as_app_get_id_filename (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->id_filename;
}

/**
 * as_app_get_categories:
 * @app: a #AsApp instance.
 *
 * Get the application categories.
 *
 * Returns: (element-type utf8) (transfer none): an array
 *
 * Since: 0.1.0
 **/
GPtrArray *
as_app_get_categories (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->categories;
}

/**
 * as_app_has_category:
 * @app: a #AsApp instance.
 * @category: a category string, e.g. "DesktopSettings"
 *
 * Searches the category list for a specific item.
 *
 * Returns: %TRUE if the application has got the specified category
 *
 * Since: 0.1.5
 */
gboolean
as_app_has_category (AsApp *app, const gchar *category)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	const gchar *tmp;
	guint i;

	for (i = 0; i < priv->categories->len; i++) {
		tmp = g_ptr_array_index (priv->categories, i);
		if (g_strcmp0 (tmp, category) == 0)
			return TRUE;
	}
	return FALSE;
}

/**
 * as_app_has_kudo:
 * @app: a #AsApp instance.
 * @kudo: a kudo string, e.g. "SearchProvider"
 *
 * Searches the kudo list for a specific item.
 *
 * Returns: %TRUE if the application has got the specified kudo
 *
 * Since: 0.2.2
 */
gboolean
as_app_has_kudo (AsApp *app, const gchar *kudo)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	const gchar *tmp;
	guint i;

	for (i = 0; i < priv->kudos->len; i++) {
		tmp = g_ptr_array_index (priv->kudos, i);
		if (g_strcmp0 (tmp, kudo) == 0)
			return TRUE;
	}
	return FALSE;
}

/**
 * as_app_has_kudo_kind:
 * @app: a #AsApp instance.
 * @kudo: a #AsKudoKind, e.g. %AS_KUDO_KIND_SEARCH_PROVIDER
 *
 * Searches the kudo list for a specific item.
 *
 * Returns: %TRUE if the application has got the specified kudo
 *
 * Since: 0.2.2
 */
gboolean
as_app_has_kudo_kind (AsApp *app, AsKudoKind kudo)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	const gchar *tmp;
	guint i;

	for (i = 0; i < priv->kudos->len; i++) {
		tmp = g_ptr_array_index (priv->kudos, i);
		if (as_kudo_kind_from_string (tmp) == kudo)
			return TRUE;
	}
	return FALSE;
}

/**
 * as_app_get_compulsory_for_desktops:
 * @app: a #AsApp instance.
 *
 * Returns the desktops where this application is compulsory.
 *
 * Returns: (element-type utf8) (transfer none): an array
 *
 * Since: 0.1.0
 **/
GPtrArray *
as_app_get_compulsory_for_desktops (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->compulsory_for_desktops;
}

/**
 * as_app_get_keywords:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 *
 * Gets any keywords the application should match against.
 *
 * Returns: (element-type utf8) (transfer none): an array, or %NULL
 *
 * Since: 0.3.0
 **/
GPtrArray *
as_app_get_keywords (AsApp *app, const gchar *locale)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	if (locale == NULL)
		locale = "C";
	return g_hash_table_lookup (priv->keywords, locale);
}

/**
 * as_app_get_kudos:
 * @app: a #AsApp instance.
 *
 * Gets any kudos the application has obtained.
 *
 * Returns: (element-type utf8) (transfer none): an array
 *
 * Since: 0.2.2
 **/
GPtrArray *
as_app_get_kudos (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->kudos;
}

/**
 * as_app_get_mimetypes:
 * @app: a #AsApp instance.
 *
 * Gets any mimetypes the application will register.
 *
 * Returns: (transfer none) (element-type utf8): an array
 *
 * Since: 0.2.0
 **/
GPtrArray *
as_app_get_mimetypes (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->mimetypes;
}

/**
 * as_app_get_releases:
 * @app: a #AsApp instance.
 *
 * Gets all the releases the application has had.
 *
 * Returns: (element-type AsRelease) (transfer none): an array
 *
 * Since: 0.1.0
 **/
GPtrArray *
as_app_get_releases (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->releases;
}

/**
 * as_app_get_provides:
 * @app: a #AsApp instance.
 *
 * Gets all the provides the application has.
 *
 * Returns: (element-type AsProvide) (transfer none): an array
 *
 * Since: 0.1.6
 **/
GPtrArray *
as_app_get_provides (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->provides;
}

/**
 * as_app_get_screenshots:
 * @app: a #AsApp instance.
 *
 * Gets any screenshots the application has defined.
 *
 * Returns: (element-type AsScreenshot) (transfer none): an array
 *
 * Since: 0.1.0
 **/
GPtrArray *
as_app_get_screenshots (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->screenshots;
}

/**
 * as_app_get_names:
 * @app: a #AsApp instance.
 *
 * Gets the names set for the application.
 *
 * Returns: (transfer none): hash table of names
 *
 * Since: 0.1.6
 **/
GHashTable *
as_app_get_names (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->names;
}

/**
 * as_app_get_comments:
 * @app: a #AsApp instance.
 *
 * Gets the comments set for the application.
 *
 * Returns: (transfer none): hash table of comments
 *
 * Since: 0.1.6
 **/
GHashTable *
as_app_get_comments (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->comments;
}

/**
 * as_app_get_developer_names:
 * @app: a #AsApp instance.
 *
 * Gets the developer_names set for the application.
 *
 * Returns: (transfer none): hash table of developer_names
 *
 * Since: 0.1.8
 **/
GHashTable *
as_app_get_developer_names (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->developer_names;
}

/**
 * as_app_get_metadata:
 * @app: a #AsApp instance.
 *
 * Gets the metadata set for the application.
 *
 * Returns: (transfer none): hash table of metadata
 *
 * Since: 0.1.6
 **/
GHashTable *
as_app_get_metadata (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->metadata;
}

/**
 * as_app_get_descriptions:
 * @app: a #AsApp instance.
 *
 * Gets the descriptions set for the application.
 *
 * Returns: (transfer none): hash table of descriptions
 *
 * Since: 0.1.6
 **/
GHashTable *
as_app_get_descriptions (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->descriptions;
}

/**
 * as_app_get_urls:
 * @app: a #AsApp instance.
 *
 * Gets the URLs set for the application.
 *
 * Returns: (transfer none): hash table of URLs
 *
 * Since: 0.1.0
 **/
GHashTable *
as_app_get_urls (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->urls;
}

/**
 * as_app_get_pkgnames:
 * @app: a #AsApp instance.
 *
 * Gets the package names (if any) for the application.
 *
 * Returns: (element-type utf8) (transfer none): an array
 *
 * Since: 0.1.0
 **/
GPtrArray *
as_app_get_pkgnames (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->pkgnames;
}

/**
 * as_app_get_architectures:
 * @app: a #AsApp instance.
 *
 * Gets the supported architectures for the application, or an empty list
 * if all architectures are supported.
 *
 * Returns: (element-type utf8) (transfer none): an array
 *
 * Since: 0.1.1
 **/
GPtrArray *
as_app_get_architectures (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->architectures;
}

/**
 * as_app_get_extends:
 * @app: a #AsApp instance.
 *
 * Gets the IDs that are extended from the addon.
 *
 * Returns: (element-type utf8) (transfer none): an array
 *
 * Since: 0.1.7
 **/
GPtrArray *
as_app_get_extends (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->extends;
}

/**
 * as_app_get_addons:
 * @app: a #AsApp instance.
 *
 * Gets all the addons the application has.
 *
 * Returns: (element-type AsApp) (transfer none): an array
 *
 * Since: 0.1.7
 **/
GPtrArray *
as_app_get_addons (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->addons;
}

/**
 * as_app_get_id_kind:
 * @app: a #AsApp instance.
 *
 * Gets the ID kind.
 *
 * Returns: enumerated value
 *
 * Since: 0.1.0
 **/
AsIdKind
as_app_get_id_kind (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->id_kind;
}

/**
 * as_app_get_name_size: (skip)
 * @app: a #AsApp instance.
 *
 * Gets the number of names.
 *
 * Returns: integer
 *
 * Since: 0.1.4
 **/
guint
as_app_get_name_size (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return g_hash_table_size (priv->names);
}

/**
 * as_app_get_comment_size: (skip)
 * @app: a #AsApp instance.
 *
 * Gets the number of comments.
 *
 * Returns: integer
 *
 * Since: 0.1.4
 **/
guint
as_app_get_comment_size (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return g_hash_table_size (priv->comments);
}

/**
 * as_app_get_description_size: (skip)
 * @app: a #AsApp instance.
 *
 * Gets the number of descriptions.
 *
 * Returns: integer
 *
 * Since: 0.1.4
 **/
guint
as_app_get_description_size (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return g_hash_table_size (priv->descriptions);
}

/**
 * as_app_get_source_kind:
 * @app: a #AsApp instance.
 *
 * Gets the source kind, i.e. where the AsApp came from.
 *
 * Returns: enumerated value
 *
 * Since: 0.1.4
 **/
AsAppSourceKind
as_app_get_source_kind (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->source_kind;
}

/**
 * as_app_get_state:
 * @app: a #AsApp instance.
 *
 * Gets the application state.
 *
 * Returns: enumerated value
 *
 * Since: 0.2.2
 **/
AsAppState
as_app_get_state (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->state;
}

/**
 * as_app_get_trust_flags:
 * @app: a #AsApp instance.
 *
 * Gets the trust flags, i.e. how trusted the incoming data is.
 *
 * Returns: bitfield
 *
 * Since: 0.2.2
 **/
AsAppTrustFlags
as_app_get_trust_flags (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->trust_flags;
}

/**
 * as_app_get_problems: (skip)
 * @app: a #AsApp instance.
 *
 * Gets the bitfield of problems.
 *
 * Returns: problems encountered during parsing the application
 *
 * Since: 0.1.4
 **/
AsAppProblems
as_app_get_problems (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->problems;
}

/**
 * as_app_get_icon_kind:
 * @app: a #AsApp instance.
 *
 * Gets the icon kind.
 *
 * Returns: enumerated value
 *
 * Since: 0.1.0
 **/
AsIconKind
as_app_get_icon_kind (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->icon_kind;
}

/**
 * as_app_get_icon:
 * @app: a #AsApp instance.
 *
 * Gets the application icon. Use as_app_get_icon_path() if you need the create
 * a full filename.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_icon (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->icon;
}

/**
 * as_app_get_pkgname_default:
 * @app: a #AsApp instance.
 *
 * Gets the default package name.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.2.0
 **/
const gchar *
as_app_get_pkgname_default (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	if (priv->pkgnames->len < 1)
		return NULL;
	return g_ptr_array_index (priv->pkgnames, 0);
}

/**
 * as_app_get_source_pkgname:
 * @app: a #AsApp instance.
 *
 * Gets the source package name that produced the binary package.
 * Only source packages producing more than one binary package will have this
 * entry set.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.2.4
 **/
const gchar *
as_app_get_source_pkgname (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->source_pkgname;
}

/**
 * as_app_get_icon_path:
 * @app: a #AsApp instance.
 *
 * Gets the application icon path.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_icon_path (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->icon_path;
}

/**
 * as_app_get_name:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 *
 * Gets the application name for a specific locale.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_name (AsApp *app, const gchar *locale)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return as_hash_lookup_by_locale (priv->names, locale);
}

/**
 * as_app_get_comment:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 *
 * Gets the application summary for a specific locale.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_comment (AsApp *app, const gchar *locale)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return as_hash_lookup_by_locale (priv->comments, locale);
}

/**
 * as_app_get_developer_name:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 *
 * Gets the application developer name for a specific locale.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.8
 **/
const gchar *
as_app_get_developer_name (AsApp *app, const gchar *locale)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return as_hash_lookup_by_locale (priv->developer_names, locale);
}

/**
 * as_app_get_description:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 *
 * Gets the application description markup for a specific locale.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_description (AsApp *app, const gchar *locale)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return as_hash_lookup_by_locale (priv->descriptions, locale);
}

/**
 * as_app_get_language:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 *
 * Gets the language coverage for the specific language.
 *
 * Returns: a percentage value where 0 is unspecified, or -1 for not found
 *
 * Since: 0.1.0
 **/
gint
as_app_get_language (AsApp *app, const gchar *locale)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	gboolean ret;
	gpointer value = NULL;

	if (locale == NULL)
		locale = "C";
	ret = g_hash_table_lookup_extended (priv->languages,
					    locale, NULL, &value);
	if (!ret)
		return -1;
	return GPOINTER_TO_INT (value);
}

/**
 * as_app_get_priority:
 * @app: a #AsApp instance.
 *
 * Gets the application priority. Larger values trump smaller values.
 *
 * Returns: priority value
 *
 * Since: 0.1.0
 **/
gint
as_app_get_priority (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->priority;
}

/**
 * as_app_get_languages:
 * @app: a #AsApp instance.
 *
 * Get a list of all languages.
 *
 * Returns: (transfer container) (element-type utf8): list of language values
 *
 * Since: 0.1.0
 **/
GList *
as_app_get_languages (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return g_hash_table_get_keys (priv->languages);
}

/**
 * as_app_get_url_item:
 * @app: a #AsApp instance.
 * @url_kind: the URL kind, e.g. %AS_URL_KIND_HOMEPAGE.
 *
 * Gets a URL.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_url_item (AsApp *app, AsUrlKind url_kind)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return g_hash_table_lookup (priv->urls,
				    as_url_kind_to_string (url_kind));
}

/**
 * as_app_get_metadata_item:
 * @app: a #AsApp instance.
 * @key: the metadata key.
 *
 * Gets some metadata item.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_metadata_item (AsApp *app, const gchar *key)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return g_hash_table_lookup (priv->metadata, key);
}

/**
 * as_app_get_project_group:
 * @app: a #AsApp instance.
 *
 * Gets an application project group.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_project_group (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->project_group;
}

/**
 * as_app_get_project_license:
 * @app: a #AsApp instance.
 *
 * Gets the application project license.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.0
 **/
const gchar *
as_app_get_project_license (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->project_license;
}

/**
 * as_app_get_metadata_license:
 * @app: a #AsApp instance.
 *
 * Gets the application project license.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.4
 **/
const gchar *
as_app_get_metadata_license (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->metadata_license;
}

/**
 * as_app_get_update_contact:
 * @app: a #AsApp instance.
 *
 * Gets the application upstream update contact email.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.1.4
 **/
const gchar *
as_app_get_update_contact (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->update_contact;
}

/**
 * as_app_get_source_file:
 * @app: a #AsApp instance.
 *
 * Gets the source filename the instance was populated from.
 *
 * NOTE: this is not set for %AS_APP_SOURCE_KIND_APPSTREAM entries.
 *
 * Returns: string, or %NULL if unset
 *
 * Since: 0.2.2
 **/
const gchar *
as_app_get_source_file (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->source_file;
}

/**
 * as_app_validate_utf8:
 **/
static gboolean
as_app_validate_utf8 (const gchar *text, gssize text_len)
{
	gboolean is_whitespace = TRUE;
	guint i;

	/* nothing */
	if (text == NULL)
		return TRUE;
	if (text[0] == '\0')
		return TRUE;

	/* is just whitespace */
	for (i = 0; text[i] != '\0' && is_whitespace; i++)
		is_whitespace = g_ascii_isspace (text[i]);
	if (is_whitespace)
		return FALSE;

	/* standard UTF-8 checks */
	if (!g_utf8_validate (text, text_len, NULL))
		return FALSE;

	/* additional check for xmllint */
	for (i = 0; text[i] != '\0'; i++) {
		if (text[i] == 0x1f)
			return FALSE;
	}
	return TRUE;
}

/******************************************************************************/

/**
 * as_app_set_id:
 * @app: a #AsApp instance.
 * @id: the new _full_ application ID, e.g. "org.gnome.Software.desktop".
 * @id_len: the size of @id, or -1 if %NULL-terminated.
 *
 * Sets a new application ID. Any invalid characters will be automatically replaced.
 *
 * Since: 0.1.0
 **/
void
as_app_set_id (AsApp *app, const gchar *id, gssize id_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	gchar *tmp;

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (id, id_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	g_free (priv->id);
	g_free (priv->id_filename);

	priv->id = as_strndup (id, id_len);
	priv->id_filename = g_strdup (priv->id);
	g_strdelimit (priv->id_filename, "&<>", '-');
	tmp = g_strrstr_len (priv->id_filename, -1, ".");
	if (tmp != NULL)
		*tmp = '\0';
}

/**
 * as_app_set_source_kind:
 * @app: a #AsApp instance.
 * @source_kind: the #AsAppSourceKind.
 *
 * Sets the source kind.
 *
 * Since: 0.1.4
 **/
void
as_app_set_source_kind (AsApp *app, AsAppSourceKind source_kind)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	priv->source_kind = source_kind;
}

/**
 * as_app_set_state:
 * @app: a #AsApp instance.
 * @state: the #AsAppState.
 *
 * Sets the application state.
 *
 * Since: 0.2.2
 **/
void
as_app_set_state (AsApp *app, AsAppState state)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	priv->state = state;
}

/**
 * as_app_set_trust_flags:
 * @app: a #AsApp instance.
 * @trust_flags: the #AsAppSourceKind.
 *
 * Sets the check flags, where %AS_APP_TRUST_FLAG_COMPLETE is completely
 * trusted input.
 *
 * Since: 0.2.2
 **/
void
as_app_set_trust_flags (AsApp *app, AsAppTrustFlags trust_flags)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	priv->trust_flags = trust_flags;
}

/**
 * as_app_set_id_kind:
 * @app: a #AsApp instance.
 * @id_kind: the #AsIdKind.
 *
 * Sets the application kind.
 *
 * Since: 0.1.0
 **/
void
as_app_set_id_kind (AsApp *app, AsIdKind id_kind)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	priv->id_kind = id_kind;
}

/**
 * as_app_set_project_group:
 * @app: a #AsApp instance.
 * @project_group: the project group, e.g. "GNOME".
 * @project_group_len: the size of @project_group, or -1 if %NULL-terminated.
 *
 * Set any project affiliation.
 *
 * Since: 0.1.0
 **/
void
as_app_set_project_group (AsApp *app,
			  const gchar *project_group,
			  gssize project_group_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (project_group, project_group_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	g_free (priv->project_group);
	priv->project_group = as_strndup (project_group, project_group_len);
}

/**
 * as_app_set_project_license:
 * @app: a #AsApp instance.
 * @project_license: the project license string.
 * @project_license_len: the size of @project_license, or -1 if %NULL-terminated.
 *
 * Set the project license.
 *
 * Since: 0.1.0
 **/
void
as_app_set_project_license (AsApp *app,
			    const gchar *project_license,
			    gssize project_license_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (project_license, project_license_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	g_free (priv->project_license);
	priv->project_license = as_strndup (project_license, project_license_len);
}

/**
 * as_app_set_metadata_license:
 * @app: a #AsApp instance.
 * @metadata_license: the project license string.
 * @metadata_license_len: the size of @metadata_license, or -1 if %NULL-terminated.
 *
 * Set the project license.
 *
 * Since: 0.1.4
 **/
void
as_app_set_metadata_license (AsApp *app,
			     const gchar *metadata_license,
			     gssize metadata_license_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	_cleanup_strv_free_ gchar **tokens = NULL;

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (metadata_license, metadata_license_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	/* automatically replace deprecated license names */
	g_free (priv->metadata_license);
	tokens = as_utils_spdx_license_tokenize (metadata_license);
	priv->metadata_license = as_utils_spdx_license_detokenize (tokens);
}

/**
 * as_app_set_source_pkgname:
 * @app: a #AsApp instance.
 * @source_pkgname: the project license string.
 * @source_pkgname_len: the size of @source_pkgname, or -1 if %NULL-terminated.
 *
 * Set the project license.
 *
 * Since: 0.2.4
 **/
void
as_app_set_source_pkgname (AsApp *app,
			     const gchar *source_pkgname,
			     gssize source_pkgname_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (source_pkgname, source_pkgname_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}
	g_free (priv->source_pkgname);
	priv->source_pkgname = as_strndup (source_pkgname, source_pkgname_len);
}

/**
 * as_app_set_source_file:
 * @app: a #AsApp instance.
 * @source_file: the filename.
 *
 * Set the file that the instance was sourced from.
 *
 * Since: 0.2.2
 **/
void
as_app_set_source_file (AsApp *app, const gchar *source_file)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	g_free (priv->source_file);
	priv->source_file = g_strdup (source_file);
}

/**
 * as_app_set_update_contact:
 * @app: a #AsApp instance.
 * @update_contact: the project license string.
 * @update_contact_len: the size of @update_contact, or -1 if %NULL-terminated.
 *
 * Set the project license.
 *
 * Since: 0.1.4
 **/
void
as_app_set_update_contact (AsApp *app,
			   const gchar *update_contact,
			   gssize update_contact_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	gboolean done_replacement = TRUE;
	gchar *tmp;
	gsize len;
	guint i;
	struct {
		const gchar	*search;
		const gchar	 replace;
	} replacements[] =  {
		{ "(@)",	'@' },
		{ " _at_ ",	'@' },
		{ "_at_",	'@' },
		{ "(at)",	'@' },
		{ " AT ",	'@' },
		{ "_dot_",	'.' },
		{ " DOT ",	'.' },
		{ NULL,		'\0' } };

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (update_contact, update_contact_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	/* copy as-is */
	g_free (priv->update_contact);
	priv->update_contact = as_strndup (update_contact, update_contact_len);
	if (priv->update_contact == NULL)
		return;

	/* keep going until we have no more matches */
	len = strlen (priv->update_contact);
	while (done_replacement) {
		done_replacement = FALSE;
		for (i = 0; replacements[i].search != NULL; i++) {
			tmp = g_strstr_len (priv->update_contact, -1,
					    replacements[i].search);
			if (tmp != NULL) {
				*tmp = replacements[i].replace;
				g_strlcpy (tmp + 1,
					   tmp + strlen (replacements[i].search),
					   len);
				done_replacement = TRUE;
			}
		}
	}
}

/**
 * as_app_set_icon:
 * @app: a #AsApp instance.
 * @icon: the icon filename or URL.
 * @icon_len: the size of @icon, or -1 if %NULL-terminated.
 *
 * Set the application icon.
 *
 * Since: 0.1.0
 **/
void
as_app_set_icon (AsApp *app, const gchar *icon, gssize icon_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (icon, icon_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	g_free (priv->icon);
	priv->icon = as_strndup (icon, icon_len);
}

/**
 * as_app_set_icon_path:
 * @app: a #AsApp instance.
 * @icon_path: the local path.
 * @icon_path_len: the size of @icon_path, or -1 if %NULL-terminated.
 *
 * Sets the icon path, where local icons would be found.
 *
 * Since: 0.1.0
 **/
void
as_app_set_icon_path (AsApp *app, const gchar *icon_path, gssize icon_path_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (icon_path, icon_path_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	g_free (priv->icon_path);
	priv->icon_path = as_strndup (icon_path, icon_path_len);
}

/**
 * as_app_set_icon_kind:
 * @app: a #AsApp instance.
 * @icon_kind: the #AsIconKind.
 *
 * Sets the icon kind.
 *
 * Since: 0.1.0
 **/
void
as_app_set_icon_kind (AsApp *app, AsIconKind icon_kind)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	priv->icon_kind = icon_kind;
}

/**
 * as_app_parse_locale:
 **/
static gchar *
as_app_parse_locale (const gchar *locale)
{
	gchar *tmp;

	if (locale == NULL)
		return g_strdup ("C");
	if (g_strcmp0 (locale, "xx") == 0)
		return NULL;
	if (g_strcmp0 (locale, "x-test") == 0)
		return NULL;
	tmp = g_strdup (locale);
	g_strdelimit (tmp, "-", '_');
	return tmp;
}

/**
 * as_app_set_name:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 * @name: the application name.
 * @name_len: the size of @name, or -1 if %NULL-terminated.
 *
 * Sets the application name for a specific locale.
 *
 * Since: 0.1.0
 **/
void
as_app_set_name (AsApp *app,
		 const gchar *locale,
		 const gchar *name,
		 gssize name_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	gchar *tmp_locale;

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (name, name_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	/* get fixed locale */
	tmp_locale = as_app_parse_locale (locale);
	if (tmp_locale == NULL)
		return;
	g_hash_table_insert (priv->names,
			     tmp_locale,
			     as_strndup (name, name_len));
}

/**
 * as_app_set_comment:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 * @comment: the application summary.
 * @comment_len: the size of @comment, or -1 if %NULL-terminated.
 *
 * Sets the application summary for a specific locale.
 *
 * Since: 0.1.0
 **/
void
as_app_set_comment (AsApp *app,
		    const gchar *locale,
		    const gchar *comment,
		    gssize comment_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	gchar *tmp_locale;

	g_return_if_fail (comment != NULL);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (comment, comment_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	/* get fixed locale */
	tmp_locale = as_app_parse_locale (locale);
	if (tmp_locale == NULL)
		return;
	g_hash_table_insert (priv->comments,
			     tmp_locale,
			     as_strndup (comment, comment_len));
}

/**
 * as_app_set_developer_name:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 * @developer_name: the application developer name.
 * @developer_name_len: the size of @developer_name, or -1 if %NULL-terminated.
 *
 * Sets the application developer name for a specific locale.
 *
 * Since: 0.1.0
 **/
void
as_app_set_developer_name (AsApp *app,
			   const gchar *locale,
			   const gchar *developer_name,
			   gssize developer_name_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	gchar *tmp_locale;

	g_return_if_fail (developer_name != NULL);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (developer_name, developer_name_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	/* get fixed locale */
	tmp_locale = as_app_parse_locale (locale);
	if (tmp_locale == NULL)
		return;
	g_hash_table_insert (priv->developer_names,
			     tmp_locale,
			     as_strndup (developer_name, developer_name_len));
}

/**
 * as_app_set_description:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 * @description: the application description.
 * @description_len: the size of @description, or -1 if %NULL-terminated.
 *
 * Sets the application descrption markup for a specific locale.
 *
 * Since: 0.1.0
 **/
void
as_app_set_description (AsApp *app,
			const gchar *locale,
			const gchar *description,
			gssize description_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	gchar *tmp_locale;

	g_return_if_fail (description != NULL);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (description, description_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	/* get fixed locale */
	tmp_locale = as_app_parse_locale (locale);
	if (tmp_locale == NULL)
		return;
	g_hash_table_insert (priv->descriptions,
			     tmp_locale,
			     as_strndup (description, description_len));
}

/**
 * as_app_set_priority:
 * @app: a #AsApp instance.
 * @priority: the priority.
 *
 * Sets the application priority, where 0 is default and positive numbers
 * are better than negative numbers.
 *
 * Since: 0.1.0
 **/
void
as_app_set_priority (AsApp *app, gint priority)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	priv->priority = priority;
}

/**
 * as_app_array_find_string:
 **/
static gboolean
as_app_array_find_string (GPtrArray *array, const gchar *value, gssize value_len)
{
	const gchar *tmp;
	guint i;

	for (i = 0; i < array->len; i++) {
		tmp = g_ptr_array_index (array, i);
		if (g_strcmp0 (tmp, value) == 0)
			return TRUE;
	}
	return FALSE;
}

/**
 * as_app_add_category:
 * @app: a #AsApp instance.
 * @category: the category.
 * @category_len: the size of @category, or -1 if %NULL-terminated.
 *
 * Adds a menu category to the application.
 *
 * Since: 0.1.0
 **/
void
as_app_add_category (AsApp *app, const gchar *category, gssize category_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (category, category_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0 &&
	    as_app_array_find_string (priv->categories, category, category_len)) {
		return;
	}

	/* simple substitution */
	if (g_strcmp0 (category, "Feed") == 0)
		category = "News";

	g_ptr_array_add (priv->categories, as_strndup (category, category_len));
}


/**
 * as_app_add_compulsory_for_desktop:
 * @app: a #AsApp instance.
 * @compulsory_for_desktop: the desktop string, e.g. "GNOME".
 * @compulsory_for_desktop_len: the size of @compulsory_for_desktop, or -1 if %NULL-terminated.
 *
 * Adds a desktop that requires this application to be installed.
 *
 * Since: 0.1.0
 **/
void
as_app_add_compulsory_for_desktop (AsApp *app,
				   const gchar *compulsory_for_desktop,
				   gssize compulsory_for_desktop_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (compulsory_for_desktop, compulsory_for_desktop_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0 &&
	    as_app_array_find_string (priv->compulsory_for_desktops,
				      compulsory_for_desktop,
				      compulsory_for_desktop_len)) {
		return;
	}

	g_ptr_array_add (priv->compulsory_for_desktops,
			 as_strndup (compulsory_for_desktop,
				     compulsory_for_desktop_len));
}

/**
 * as_app_add_keyword:
 * @app: a #AsApp instance.
 * @locale: the locale, or %NULL. e.g. "en_GB"
 * @keyword: the keyword.
 * @keyword_len: the size of @keyword, or -1 if %NULL-terminated.
 *
 * Add a keyword the application should match against.
 *
 * Since: 0.3.0
 **/
void
as_app_add_keyword (AsApp *app,
		    const gchar *locale,
		    const gchar *keyword,
		    gssize keyword_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	GPtrArray *tmp;
	_cleanup_free_ gchar *tmp_locale = NULL;

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (keyword, keyword_len)) {
		return;
	}

	/* get fixed locale */
	tmp_locale = as_app_parse_locale (locale);
	if (tmp_locale == NULL)
		return;

	/* create an array if required */
	tmp = g_hash_table_lookup (priv->keywords, tmp_locale);
	if (tmp == NULL) {
		tmp = g_ptr_array_new_with_free_func (g_free);
		g_hash_table_insert (priv->keywords, g_strdup (tmp_locale), tmp);
	} else if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0) {
		if (as_app_array_find_string (tmp, keyword, keyword_len))
			return;
	}
	g_ptr_array_add (tmp, as_strndup (keyword, keyword_len));
}

/**
 * as_app_add_kudo:
 * @app: a #AsApp instance.
 * @kudo: the kudo.
 * @kudo_len: the size of @kudo, or -1 if %NULL-terminated.
 *
 * Add a kudo the application has obtained.
 *
 * Since: 0.2.2
 **/
void
as_app_add_kudo (AsApp *app, const gchar *kudo, gssize kudo_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (kudo, kudo_len)) {
		return;
	}
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0 &&
	    as_app_array_find_string (priv->kudos, kudo, kudo_len)) {
		return;
	}
	g_ptr_array_add (priv->kudos, as_strndup (kudo, kudo_len));
}

/**
 * as_app_add_kudo_kind:
 * @app: a #AsApp instance.
 * @kudo_kind: the #AsKudoKind.
 *
 * Add a kudo the application has obtained.
 *
 * Since: 0.2.2
 **/
void
as_app_add_kudo_kind (AsApp *app, AsKudoKind kudo_kind)
{
	as_app_add_kudo (app, as_kudo_kind_to_string (kudo_kind), -1);
}

/**
 * as_app_add_mimetype:
 * @app: a #AsApp instance.
 * @mimetype: the mimetype.
 * @mimetype_len: the size of @mimetype, or -1 if %NULL-terminated.
 *
 * Adds a mimetype the application can process.
 *
 * Since: 0.1.0
 **/
void
as_app_add_mimetype (AsApp *app, const gchar *mimetype, gssize mimetype_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (mimetype, mimetype_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0 &&
	    as_app_array_find_string (priv->mimetypes, mimetype, mimetype_len)) {
		return;
	}

	g_ptr_array_add (priv->mimetypes, as_strndup (mimetype, mimetype_len));
}

/**
 * as_app_add_release:
 * @app: a #AsApp instance.
 * @release: a #AsRelease instance.
 *
 * Adds a release to an application.
 *
 * Since: 0.1.0
 **/
void
as_app_add_release (AsApp *app, AsRelease *release)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	g_ptr_array_add (priv->releases, g_object_ref (release));
}

/**
 * as_app_add_provide:
 * @app: a #AsApp instance.
 * @provide: a #AsProvide instance.
 *
 * Adds a provide to an application.
 *
 * Since: 0.1.6
 **/
void
as_app_add_provide (AsApp *app, AsProvide *provide)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	g_ptr_array_add (priv->provides, g_object_ref (provide));
}

/**
 * as_app_add_screenshot:
 * @app: a #AsApp instance.
 * @screenshot: a #AsScreenshot instance.
 *
 * Adds a screenshot to an application.
 *
 * Since: 0.1.0
 **/
void
as_app_add_screenshot (AsApp *app, AsScreenshot *screenshot)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0) {
		AsScreenshot *ss;
		guint i;
		for (i = 0; i < priv->screenshots->len; i++) {
			ss = g_ptr_array_index (priv->screenshots, i);
			if (ss == screenshot)
				return;
		}
	}

	g_ptr_array_add (priv->screenshots, g_object_ref (screenshot));
}

/**
 * as_app_add_pkgname:
 * @app: a #AsApp instance.
 * @pkgname: the package name.
 * @pkgname_len: the size of @pkgname, or -1 if %NULL-terminated.
 *
 * Adds a package name to an application.
 *
 * Since: 0.1.0
 **/
void
as_app_add_pkgname (AsApp *app, const gchar *pkgname, gssize pkgname_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (pkgname, pkgname_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0 &&
	    as_app_array_find_string (priv->pkgnames, pkgname, pkgname_len)) {
		return;
	}

	g_ptr_array_add (priv->pkgnames, as_strndup (pkgname, pkgname_len));
}

/**
 * as_app_add_arch:
 * @app: a #AsApp instance.
 * @arch: the package name.
 * @arch_len: the size of @arch, or -1 if %NULL-terminated.
 *
 * Adds a package name to an application.
 *
 * Since: 0.1.1
 **/
void
as_app_add_arch (AsApp *app, const gchar *arch, gssize arch_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (arch, arch_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0 &&
	    as_app_array_find_string (priv->architectures, arch, arch_len)) {
		return;
	}

	g_ptr_array_add (priv->architectures, as_strndup (arch, arch_len));
}

/**
 * as_app_add_language:
 * @app: a #AsApp instance.
 * @percentage: the percentage completion of the translation, or 0 for unknown
 * @locale: the locale, or %NULL. e.g. "en_GB"
 * @locale_len: the size of @locale, or -1 if %NULL-terminated.
 *
 * Adds a language to the application.
 *
 * Since: 0.1.0
 **/
void
as_app_add_language (AsApp *app,
		     gint percentage,
		     const gchar *locale,
		     gssize locale_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (locale, locale_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	if (locale == NULL)
		locale = "C";
	g_hash_table_insert (priv->languages,
			     as_strndup (locale, locale_len),
			     GINT_TO_POINTER (percentage));
}

/**
 * as_app_add_url:
 * @app: a #AsApp instance.
 * @url_kind: the URL kind, e.g. %AS_URL_KIND_HOMEPAGE
 * @url: the full URL.
 * @url_len: the size of @url, or -1 if %NULL-terminated.
 *
 * Adds some URL data to the application.
 *
 * Since: 0.1.0
 **/
void
as_app_add_url (AsApp *app,
		AsUrlKind url_kind,
		const gchar *url,
		gssize url_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (url, url_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}

	g_hash_table_insert (priv->urls,
			     g_strdup (as_url_kind_to_string (url_kind)),
			     as_strndup (url, url_len));
}

/**
 * as_app_add_metadata:
 * @app: a #AsApp instance.
 * @key: the metadata key.
 * @value: the value to store.
 * @value_len: the size of @value, or -1 if %NULL-terminated.
 *
 * Adds a metadata entry to the application.
 *
 * Since: 0.1.0
 **/
void
as_app_add_metadata (AsApp *app,
		     const gchar *key,
		     const gchar *value,
		     gssize value_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	g_return_if_fail (key != NULL);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (value, value_len)) {
		return;
	}

	if (value == NULL)
		value = "";
	g_hash_table_insert (priv->metadata,
			     g_strdup (key),
			     as_strndup (value, value_len));
}

/**
 * as_app_remove_metadata:
 * @app: a #AsApp instance.
 * @key: the metadata key.
 *
 * Removes a metadata item from the application.
 *
 * Since: 0.1.0
 **/
void
as_app_remove_metadata (AsApp *app, const gchar *key)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	g_hash_table_remove (priv->metadata, key);
}

/**
 * as_app_add_extends:
 * @app: a #AsApp instance.
 * @extends: the full ID, e.g. "eclipse.desktop".
 * @extends_len: the size of @extends, or -1 if %NULL-terminated.
 *
 * Adds a parent ID to the application.
 *
 * Since: 0.1.7
 **/
void
as_app_add_extends (AsApp *app, const gchar *extends, gssize extends_len)
{
	AsAppPrivate *priv = GET_PRIVATE (app);

	/* handle untrusted */
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_VALID_UTF8) > 0 &&
	    !as_app_validate_utf8 (extends, extends_len)) {
		priv->problems |= AS_APP_PROBLEM_NOT_VALID_UTF8;
		return;
	}
	if ((priv->trust_flags & AS_APP_TRUST_FLAG_CHECK_DUPLICATES) > 0 &&
	    as_app_array_find_string (priv->extends, extends, extends_len)) {
		return;
	}

	g_ptr_array_add (priv->extends, as_strndup (extends, extends_len));
}

/**
 * as_app_add_addon:
 * @app: a #AsApp instance.
 * @addon: a #AsApp instance.
 *
 * Adds a addon to an application.
 *
 * Since: 0.1.7
 **/
void
as_app_add_addon (AsApp *app, AsApp *addon)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	g_ptr_array_add (priv->addons, g_object_ref (addon));
}

/******************************************************************************/


/**
 * as_app_subsume_dict:
 **/
static void
as_app_subsume_dict (GHashTable *dest, GHashTable *src, gboolean overwrite)
{
	GList *l;
	const gchar *tmp;
	const gchar *key;
	const gchar *value;
	_cleanup_list_free_ GList *keys;

	keys = g_hash_table_get_keys (src);
	for (l = keys; l != NULL; l = l->next) {
		key = l->data;
		if (!overwrite) {
			tmp = g_hash_table_lookup (dest, key);
			if (tmp != NULL)
				continue;
		}
		value = g_hash_table_lookup (src, key);
		g_hash_table_insert (dest, g_strdup (key), g_strdup (value));
	}
}

/**
 * as_app_subsume_keywords:
 **/
static void
as_app_subsume_keywords (AsApp *app, AsApp *donor, gboolean overwrite)
{
	AsAppPrivate *priv = GET_PRIVATE (donor);
	GList *l;
	GPtrArray *array;
	const gchar *key;
	const gchar *tmp;
	guint i;
	_cleanup_list_free_ GList *keys = NULL;

	/* get all locales in the keywords dict */
	keys = g_hash_table_get_keys (priv->keywords);
	for (l = keys; l != NULL; l = l->next) {
		key = l->data;
		if (!overwrite) {
			array = as_app_get_keywords (app, key);
			if (array != NULL)
				continue;
		}

		/* get the array of keywords for the specific locale */
		array = g_hash_table_lookup (priv->keywords, key);
		if (array == NULL)
			continue;
		for (i = 0; i < array->len; i++) {
			tmp = g_ptr_array_index (array, i);
			as_app_add_keyword (app, key, tmp, -1);
		}
	}
}

/**
 * as_app_subsume_private:
 **/
static void
as_app_subsume_private (AsApp *app, AsApp *donor, AsAppSubsumeFlags flags)
{
	AsAppPrivate *priv = GET_PRIVATE (donor);
	AsAppPrivate *papp = GET_PRIVATE (app);
	AsScreenshot *ss;
	const gchar *tmp;
	const gchar *key;
	gboolean overwrite;
	guint i;
	gint percentage;
	GList *l;
	_cleanup_list_free_ GList *keys = NULL;

	/* stop us shooting ourselves in the foot */
	papp->trust_flags |= AS_APP_TRUST_FLAG_CHECK_DUPLICATES;

	overwrite = (flags & AS_APP_SUBSUME_FLAG_NO_OVERWRITE) == 0;

	/* id-kind */
	if (papp->id_kind == AS_ID_KIND_UNKNOWN)
		as_app_set_id_kind (app, priv->id_kind);

	/* AppData or AppStream can overwrite the id-kind of desktop files */
	if ((priv->source_kind == AS_APP_SOURCE_KIND_APPDATA ||
	     priv->source_kind == AS_APP_SOURCE_KIND_APPSTREAM) &&
	    papp->source_kind == AS_APP_SOURCE_KIND_DESKTOP)
		as_app_set_id_kind (app, priv->id_kind);

	/* state */
	if (papp->state == AS_APP_STATE_UNKNOWN)
		as_app_set_state (app, priv->state);

	/* pkgnames */
	for (i = 0; i < priv->pkgnames->len; i++) {
		tmp = g_ptr_array_index (priv->pkgnames, i);
		as_app_add_pkgname (app, tmp, -1);
	}

	/* kudos */
	for (i = 0; i < priv->kudos->len; i++) {
		tmp = g_ptr_array_index (priv->kudos, i);
		as_app_add_kudo (app, tmp, -1);
	}

	/* extends */
	for (i = 0; i < priv->extends->len; i++) {
		tmp = g_ptr_array_index (priv->extends, i);
		as_app_add_extends (app, tmp, -1);
	}

	/* compulsory_for_desktops */
	for (i = 0; i < priv->compulsory_for_desktops->len; i++) {
		tmp = g_ptr_array_index (priv->compulsory_for_desktops, i);
		as_app_add_compulsory_for_desktop (app, tmp, -1);
	}

	/* screenshots */
	for (i = 0; i < priv->screenshots->len; i++) {
		ss = g_ptr_array_index (priv->screenshots, i);
		as_app_add_screenshot (app, ss);
	}

	/* mimetypes */
	for (i = 0; i < priv->mimetypes->len; i++) {
		tmp = g_ptr_array_index (priv->mimetypes, i);
		as_app_add_mimetype (app, tmp, -1);
	}

	/* do not subsume all properties */
	if ((flags & AS_APP_SUBSUME_FLAG_PARTIAL) > 0)
		return;

	/* languages */
	keys = g_hash_table_get_keys (priv->languages);
	for (l = keys; l != NULL; l = l->next) {
		key = l->data;
		if (flags & AS_APP_SUBSUME_FLAG_NO_OVERWRITE) {
			percentage = as_app_get_language (app, key);
			if (percentage >= 0)
				continue;
		}
		percentage = GPOINTER_TO_INT (g_hash_table_lookup (priv->languages, key));
		as_app_add_language (app, percentage, key, -1);
	}

	/* dictionaries */
	as_app_subsume_dict (papp->names, priv->names, overwrite);
	as_app_subsume_dict (papp->comments, priv->comments, overwrite);
	as_app_subsume_dict (papp->developer_names, priv->developer_names, overwrite);
	as_app_subsume_dict (papp->descriptions, priv->descriptions, overwrite);
	as_app_subsume_dict (papp->metadata, priv->metadata, overwrite);
	as_app_subsume_dict (papp->urls, priv->urls, overwrite);
	as_app_subsume_keywords (app, donor, overwrite);

	/* icon */
	if (priv->icon != NULL)
		as_app_set_icon (app, priv->icon, -1);
	if (priv->icon_kind != AS_ICON_KIND_UNKNOWN)
		as_app_set_icon_kind (app, priv->icon_kind);

	/* source */
	if (priv->source_file != NULL)
		as_app_set_source_file (app, priv->source_file);

	/* project_group */
	if (priv->project_group != NULL)
		as_app_set_project_group (app, priv->project_group, -1);
}

/**
 * as_app_subsume_full:
 * @app: a #AsApp instance.
 * @donor: the donor.
 * @flags: any optional flags, e.g. %AS_APP_SUBSUME_FLAG_NO_OVERWRITE
 *
 * Copies information from the donor to the application object.
 *
 * Since: 0.1.4
 **/
void
as_app_subsume_full (AsApp *app, AsApp *donor, AsAppSubsumeFlags flags)
{
	g_assert (app != donor);

	/* two way sync implies no overwriting */
	if ((flags & AS_APP_SUBSUME_FLAG_BOTH_WAYS) > 0)
		flags |= AS_APP_SUBSUME_FLAG_NO_OVERWRITE;

	/* one way sync */
	as_app_subsume_private (app, donor, flags);

	/* and back again */
	if ((flags & AS_APP_SUBSUME_FLAG_BOTH_WAYS) > 0)
		as_app_subsume_private (donor, app, flags);
}

/**
 * as_app_subsume:
 * @app: a #AsApp instance.
 * @donor: the donor.
 *
 * Copies information from the donor to the application object.
 *
 * Since: 0.1.0
 **/
void
as_app_subsume (AsApp *app, AsApp *donor)
{
	as_app_subsume_full (app, donor, AS_APP_SUBSUME_FLAG_NONE);
}

/**
 * as_app_kudo_kind_from_legacy_string:
 **/
static AsKudoKind
as_app_kudo_kind_from_legacy_string (const gchar *legacy_name)
{
	if (g_strcmp0 (legacy_name, "X-Kudo-SearchProvider") == 0)
		return AS_KUDO_KIND_SEARCH_PROVIDER;
	if (g_strcmp0 (legacy_name, "X-Kudo-InstallsUserDocs") == 0)
		return AS_KUDO_KIND_USER_DOCS;
	if (g_strcmp0 (legacy_name, "X-Kudo-UsesAppMenu") == 0)
		return AS_KUDO_KIND_APP_MENU;
	if (g_strcmp0 (legacy_name, "X-Kudo-GTK3") == 0)
		return AS_KUDO_KIND_MODERN_TOOLKIT;
	if (g_strcmp0 (legacy_name, "X-Kudo-QT5") == 0)
		return AS_KUDO_KIND_MODERN_TOOLKIT;
	if (g_strcmp0 (legacy_name, "X-Kudo-UsesNotifications") == 0)
		return AS_KUDO_KIND_NOTIFICATIONS;
	return AS_KUDO_KIND_UNKNOWN;
}

/**
 * as_app_kudo_kind_to_legacy_string:
 **/
static const gchar *
as_app_kudo_kind_to_legacy_string (AsKudoKind kudo_kind)
{
	if (kudo_kind == AS_KUDO_KIND_SEARCH_PROVIDER)
		return "X-Kudo-SearchProvider";
	if (kudo_kind == AS_KUDO_KIND_USER_DOCS)
		return "X-Kudo-InstallsUserDocs";
	if (kudo_kind == AS_KUDO_KIND_APP_MENU)
		return "X-Kudo-UsesAppMenu";
	if (kudo_kind == AS_KUDO_KIND_MODERN_TOOLKIT)
		return "X-Kudo-GTK3";
	if (kudo_kind == AS_KUDO_KIND_NOTIFICATIONS)
		return "X-Kudo-UsesNotifications";
	return NULL;
}

/**
 * gs_app_node_language_sort_cb:
 **/
static gint
gs_app_node_language_sort_cb (gconstpointer a, gconstpointer b)
{
	return g_strcmp0 ((const gchar *) a, (const gchar *) b);
}

/**
 * as_app_node_insert_languages:
 **/
static void
as_app_node_insert_languages (AsApp *app, GNode *parent)
{
	GNode *node_tmp;
	GList *l;
	const gchar *locale;
	gchar tmp[4];
	gint percentage;
	_cleanup_list_free_ GList *langs;

	node_tmp = as_node_insert (parent, "languages", NULL, 0, NULL);
	langs = as_app_get_languages (app);
	langs = g_list_sort (langs, gs_app_node_language_sort_cb);
	for (l = langs; l != NULL; l = l->next) {
		locale = l->data;
		percentage = as_app_get_language (app, locale);
		if (percentage == 0) {
			as_node_insert (node_tmp, "lang", locale, 0, NULL);
		} else {
			g_snprintf (tmp, sizeof (tmp), "%i", percentage);
			as_node_insert (node_tmp, "lang", locale, 0,
					"percentage", tmp,
					NULL);
		}
	}
}

/**
 * as_app_ptr_array_sort_cb:
 **/
static gint
as_app_ptr_array_sort_cb (gconstpointer a, gconstpointer b)
{
	return g_strcmp0 (*((const gchar **) a), *((const gchar **) b));
}

/**
 * as_app_list_sort_cb:
 **/
static gint
as_app_list_sort_cb (gconstpointer a, gconstpointer b)
{
	return g_strcmp0 ((const gchar *) a, (const gchar *) b);
}

/**
 * as_app_node_insert_keywords:
 **/
static void
as_app_node_insert_keywords (AsApp *app, GNode *parent, gdouble api_version)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	GList *keys;
	GList *l;
	GNode *node_tmp;
	GPtrArray *keywords;
	const gchar *lang;
	const gchar *tmp;
	guint i;
	_cleanup_hashtable_unref_ GHashTable *already_in_c = NULL;

	/* don't add localized keywords that already exist in C, e.g.
	 * there's no point adding "c++" in 14 different languages */
	already_in_c = g_hash_table_new_full (g_str_hash, g_str_equal,
					      g_free, NULL);
	keywords = g_hash_table_lookup (priv->keywords, "C");
	if (keywords != NULL) {
		for (i = 0; i < keywords->len; i++) {
			tmp = g_ptr_array_index (keywords, i);
			g_hash_table_insert (already_in_c,
					     g_strdup (tmp),
					     GINT_TO_POINTER (1));
		}
	}

	keys = g_hash_table_get_keys (priv->keywords);
	keys = g_list_sort (keys, as_app_list_sort_cb);
	for (l = keys; l != NULL; l = l->next) {
		lang = l->data;
		keywords = g_hash_table_lookup (priv->keywords, lang);
		g_ptr_array_sort (keywords, as_app_ptr_array_sort_cb);
		for (i = 0; i < keywords->len; i++) {
			tmp = g_ptr_array_index (keywords, i);
			if (tmp == NULL)
				continue;
			if (g_strcmp0 (lang, "C") != 0 &&
			    g_hash_table_lookup (already_in_c, tmp) != NULL)
				continue;
			node_tmp = as_node_insert (parent,
						   "keyword", tmp,
						   0, NULL);
			if (g_strcmp0 (lang, "C") != 0) {
				as_node_add_attribute (node_tmp,
						       "xml:lang",
						       lang, -1);
			}
		}
	}
	g_list_free (keys);
}

/**
 * as_app_node_insert: (skip)
 * @app: a #AsApp instance.
 * @parent: the parent #GNode to use..
 * @api_version: the AppStream API version
 *
 * Inserts the application into the DOM tree.
 *
 * Returns: (transfer none): A populated #GNode, or %NULL
 *
 * Since: 0.1.0
 **/
GNode *
as_app_node_insert (AsApp *app, GNode *parent, gdouble api_version)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	AsRelease *rel;
	AsScreenshot *ss;
	GNode *node_app;
	GNode *node_tmp;
	const gchar *tmp;
	guint i;
	static gint old_prio_value = 0;

	/* no addons allowed here */
	if (api_version < 0.7 && priv->id_kind == AS_ID_KIND_ADDON) {
		g_warning ("Not writing addon '%s' as API version %.1f < 0.7",
			   priv->id, api_version);
		return NULL;
	}

	/* <component> or <application> */
	if (api_version >= 0.6) {
		node_app = as_node_insert (parent, "component", NULL, 0, NULL);
		if (priv->id_kind != AS_ID_KIND_UNKNOWN) {
			as_node_add_attribute (node_app,
					       "type",
					       as_id_kind_to_string (priv->id_kind),
					       -1);
		}
	} else {
		node_app = as_node_insert (parent, "application", NULL, 0, NULL);
	}

	/* <id> */
	node_tmp = as_node_insert (node_app, "id", priv->id, 0, NULL);
	if (api_version < 0.6 && priv->id_kind != AS_ID_KIND_UNKNOWN) {
		as_node_add_attribute (node_tmp,
				       "type",
				       as_id_kind_to_string (priv->id_kind),
				       -1);
	}

	/* <priority> */
	if (priv->priority != 0) {
		gchar prio[6];
		g_snprintf (prio, sizeof (prio), "%i", priv->priority);
		if (api_version >= 0.61) {
			as_node_add_attribute (node_app,
					       "priority", prio, -1);
		}
	}

	/* this looks crazy, but priority was initially implemented on
	 * the <applications> node and was designed to be per-file */
	if (api_version <= 0.5 &&
	    api_version > 0.3 &&
	    old_prio_value != priv->priority) {
		gchar prio[6];
		g_snprintf (prio, sizeof (prio), "%i", priv->priority);
		as_node_insert (parent, "priority", prio, 0, NULL);
		old_prio_value = priv->priority;
	}

	/* <pkgname> */
	g_ptr_array_sort (priv->pkgnames, as_app_ptr_array_sort_cb);
	for (i = 0; i < priv->pkgnames->len; i++) {
		tmp = g_ptr_array_index (priv->pkgnames, i);
		as_node_insert (node_app, "pkgname", tmp, 0, NULL);
	}

	/* <source_pkgname> */
	if (priv->source_pkgname != NULL && api_version >= 0.8) {
		as_node_insert (node_app, "source_pkgname",
				priv->source_pkgname, 0, NULL);
	}

	/* <name> */
	as_node_insert_localized (node_app, "name",
				  priv->names,
				  AS_NODE_INSERT_FLAG_DEDUPE_LANG);

	/* <summary> */
	as_node_insert_localized (node_app, "summary",
				  priv->comments,
				  AS_NODE_INSERT_FLAG_DEDUPE_LANG);

	/* <developer_name> */
	if (api_version >= 0.7) {
		as_node_insert_localized (node_app, "developer_name",
					  priv->developer_names,
					  AS_NODE_INSERT_FLAG_DEDUPE_LANG);
	}

	/* <description> */
	if (api_version < 0.6) {
		as_node_insert_localized (node_app, "description",
					  priv->descriptions,
					  AS_NODE_INSERT_FLAG_NO_MARKUP |
					  AS_NODE_INSERT_FLAG_DEDUPE_LANG);
	} else {
		as_node_insert_localized (node_app, "description",
					  priv->descriptions,
					  AS_NODE_INSERT_FLAG_PRE_ESCAPED |
					  AS_NODE_INSERT_FLAG_DEDUPE_LANG);
	}

	/* <icon> */
	if (priv->icon != NULL) {
		as_node_insert (node_app, "icon", priv->icon, 0,
				"type", as_icon_kind_to_string (priv->icon_kind),
				NULL);
	}

	/* <categories> */
	if (api_version >= 0.5) {
		g_ptr_array_sort (priv->categories, as_app_ptr_array_sort_cb);
		if (priv->categories->len > 0) {
			node_tmp = as_node_insert (node_app, "categories", NULL, 0, NULL);
			for (i = 0; i < priv->categories->len; i++) {
				tmp = g_ptr_array_index (priv->categories, i);
				as_node_insert (node_tmp, "category", tmp, 0, NULL);
			}
		}
	} else {
		g_ptr_array_sort (priv->categories, as_app_ptr_array_sort_cb);
		if (priv->categories->len > 0) {
			node_tmp = as_node_insert (node_app, "appcategories", NULL, 0, NULL);
			for (i = 0; i < priv->categories->len; i++) {
				tmp = g_ptr_array_index (priv->categories, i);
				as_node_insert (node_tmp, "appcategory", tmp, 0, NULL);
			}
		}
	}

	/* <architectures> */
	if (priv->architectures->len > 0 && api_version >= 0.6) {
		g_ptr_array_sort (priv->architectures, as_app_ptr_array_sort_cb);
		node_tmp = as_node_insert (node_app, "architectures", NULL, 0, NULL);
		for (i = 0; i < priv->architectures->len; i++) {
			tmp = g_ptr_array_index (priv->architectures, i);
			as_node_insert (node_tmp, "arch", tmp, 0, NULL);
		}
	}

	/* <keywords> */
	if (g_hash_table_size (priv->keywords) > 0) {
		node_tmp = as_node_insert (node_app, "keywords", NULL, 0, NULL);
		as_app_node_insert_keywords (app, node_tmp, api_version);
	}

	/* <kudos> */
	if (priv->kudos->len > 0 && api_version >= 0.8) {
		g_ptr_array_sort (priv->kudos, as_app_ptr_array_sort_cb);
		node_tmp = as_node_insert (node_app, "kudos", NULL, 0, NULL);
		for (i = 0; i < priv->kudos->len; i++) {
			tmp = g_ptr_array_index (priv->kudos, i);
			as_node_insert (node_tmp, "kudo", tmp, 0, NULL);
		}
	}

	/* <vetos> */
	if (priv->vetos->len > 0 && api_version >= 0.8) {
		g_ptr_array_sort (priv->vetos, as_app_ptr_array_sort_cb);
		node_tmp = as_node_insert (node_app, "vetos", NULL, 0, NULL);
		for (i = 0; i < priv->vetos->len; i++) {
			tmp = g_ptr_array_index (priv->vetos, i);
			as_node_insert (node_tmp, "veto", tmp, 0, NULL);
		}
	}

	/* <mimetypes> */
	if (priv->mimetypes->len > 0 && api_version >= 0.4) {
		g_ptr_array_sort (priv->mimetypes, as_app_ptr_array_sort_cb);
		node_tmp = as_node_insert (node_app, "mimetypes", NULL, 0, NULL);
		for (i = 0; i < priv->mimetypes->len; i++) {
			tmp = g_ptr_array_index (priv->mimetypes, i);
			as_node_insert (node_tmp, "mimetype", tmp, 0, NULL);
		}
	}

	/* <project_license> or <licence> */
	if (priv->project_license != NULL) {
		if (api_version >= 0.4) {
			as_node_insert (node_app, "project_license",
					priv->project_license, 0, NULL);
		} else if (api_version >= 0.31) {
			as_node_insert (node_app, "licence",
					priv->project_license, 0, NULL);
		}
	}

	/* <url> */
	as_node_insert_hash (node_app, "url", "type", priv->urls, 0);

	/* <project_group> */
	if (priv->project_group != NULL && api_version >= 0.4) {
		as_node_insert (node_app, "project_group",
				priv->project_group, 0, NULL);
	}

	/* <compulsory_for_desktop> */
	if (priv->compulsory_for_desktops != NULL && api_version >= 0.4) {
		g_ptr_array_sort (priv->compulsory_for_desktops,
				  as_app_ptr_array_sort_cb);
		for (i = 0; i < priv->compulsory_for_desktops->len; i++) {
			tmp = g_ptr_array_index (priv->compulsory_for_desktops, i);
			as_node_insert (node_app, "compulsory_for_desktop",
					tmp, 0, NULL);
		}
	}

	/* <extends> */
	if (api_version >= 0.7) {
		g_ptr_array_sort (priv->extends, as_app_ptr_array_sort_cb);
		for (i = 0; i < priv->extends->len; i++) {
			tmp = g_ptr_array_index (priv->extends, i);
			as_node_insert (node_app, "extends", tmp, 0, NULL);
		}
	}

	/* <screenshots> */
	if (priv->screenshots->len > 0 && api_version >= 0.4) {
		node_tmp = as_node_insert (node_app, "screenshots", NULL, 0, NULL);
		for (i = 0; i < priv->screenshots->len; i++) {
			ss = g_ptr_array_index (priv->screenshots, i);
			as_screenshot_node_insert (ss, node_tmp, api_version);
		}
	}

	/* <releases> */
	if (priv->releases->len > 0 && api_version >= 0.6) {
		node_tmp = as_node_insert (node_app, "releases", NULL, 0, NULL);
		for (i = 0; i < priv->releases->len && i < 3; i++) {
			rel = g_ptr_array_index (priv->releases, i);
			as_release_node_insert (rel, node_tmp, api_version);
		}
	}

	/* <provides> */
	if (priv->provides->len > 0 && api_version >= 0.6) {
		AsProvide *provide;
		node_tmp = as_node_insert (node_app, "provides", NULL, 0, NULL);
		for (i = 0; i < priv->provides->len; i++) {
			provide = g_ptr_array_index (priv->provides, i);
			as_provide_node_insert (provide, node_tmp, api_version);
		}
	}

	/* <languages> */
	if (g_hash_table_size (priv->languages) > 0 && api_version >= 0.4)
		as_app_node_insert_languages (app, node_app);

	/* <metadata> */
	if (g_hash_table_size (priv->metadata) > 0 && api_version >= 0.4) {
		node_tmp = as_node_insert (node_app, "metadata", NULL, 0, NULL);
		as_node_insert_hash (node_tmp, "value", "key", priv->metadata, FALSE);

		/* convert the kudos to the old name */
		if (priv->kudos->len > 0 && api_version < 0.8) {
			for (i = 0; i < priv->kudos->len; i++) {
				AsKudoKind kudo;
				tmp = g_ptr_array_index (priv->kudos, i);
				kudo = as_kudo_kind_from_string (tmp);
				tmp = as_app_kudo_kind_to_legacy_string (kudo);
				if (tmp != NULL) {
					as_node_insert (node_tmp, "value", NULL,
							AS_NODE_INSERT_FLAG_NONE,
							"key", tmp,
							NULL);
				}
			}
		}
	}

	return node_app;
}

/**
 * as_app_node_parse_child:
 **/
static gboolean
as_app_node_parse_child (AsApp *app, GNode *n, AsAppParseFlags flags, GError **error)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	GNode *c;
	const gchar *tmp;
	gchar *taken;

	switch (as_node_get_tag (n)) {

	/* <id> */
	case AS_TAG_ID:
		tmp = as_node_get_attribute (n, "type");
		if (tmp != NULL)
			as_app_set_id_kind (app, as_id_kind_from_string (tmp));
		as_app_set_id (app, as_node_get_data (n), -1);
		break;

	/* <priority> */
	case AS_TAG_PRIORITY:
		as_app_set_priority (app, g_ascii_strtoll (as_node_get_data (n),
							   NULL, 10));
		break;

	/* <pkgname> */
	case AS_TAG_PKGNAME:
		g_ptr_array_add (priv->pkgnames, as_node_take_data (n));
		break;

	/* <name> */
	case AS_TAG_NAME:
		taken = as_app_parse_locale (as_node_get_attribute (n, "xml:lang"));
		if (taken == NULL)
			break;
		g_hash_table_insert (priv->names,
				     taken,
				     as_node_take_data (n));
		break;

	/* <summary> */
	case AS_TAG_SUMMARY:
		taken = as_app_parse_locale (as_node_get_attribute (n, "xml:lang"));
		if (taken == NULL)
			break;
		g_hash_table_insert (priv->comments,
				     taken,
				     as_node_take_data (n));
		break;

	/* <developer_name> */
	case AS_TAG_DEVELOPER_NAME:
		taken = as_app_parse_locale (as_node_get_attribute (n, "xml:lang"));
		if (taken == NULL)
			break;
		g_hash_table_insert (priv->developer_names,
				     taken,
				     as_node_take_data (n));
		break;

	/* <description> */
	case AS_TAG_DESCRIPTION:

		/* unwrap appdata inline */
		if (priv->source_kind == AS_APP_SOURCE_KIND_APPDATA) {
			GError *error_local = NULL;
			_cleanup_hashtable_unref_ GHashTable *unwrapped;
			unwrapped = as_node_get_localized_unwrap (n, &error_local);
			if (unwrapped == NULL) {
				if (g_error_matches (error_local,
						     AS_NODE_ERROR,
						     AS_NODE_ERROR_INVALID_MARKUP)) {
					_cleanup_string_free_ GString *debug = NULL;
					debug = as_node_to_xml (n, AS_NODE_TO_XML_FLAG_NONE);
					g_warning ("ignoring description '%s' from %s: %s",
						   debug->str,
						   as_app_get_source_file (app),
						   error_local->message);
					g_error_free (error_local);
					break;
				}
				g_propagate_error (error, error_local);
				return FALSE;
			}
			as_app_subsume_dict (priv->descriptions, unwrapped, FALSE);
			break;
		}

		if (n->children == NULL) {
			/* pre-formatted */
			priv->problems |= AS_APP_PROBLEM_PREFORMATTED_DESCRIPTION;
			as_app_set_description (app,
						as_node_get_attribute (n, "xml:lang"),
						as_node_get_data (n),
						-1);
		} else {
			_cleanup_string_free_ GString *xml;
			xml = as_node_to_xml (n->children,
					      AS_NODE_TO_XML_FLAG_INCLUDE_SIBLINGS);
			as_app_set_description (app,
						as_node_get_attribute (n, "xml:lang"),
						xml->str, xml->len);
		}
		break;

	/* <icon> */
	case AS_TAG_ICON:
		tmp = as_node_get_attribute (n, "type");
		as_app_set_icon_kind (app, as_icon_kind_from_string (tmp));
		g_free (priv->icon);
		priv->icon = as_node_take_data (n);
		break;

	/* <categories> */
	case AS_TAG_CATEGORIES:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_ptr_array_set_size (priv->categories, 0);
		for (c = n->children; c != NULL; c = c->next) {
			if (as_node_get_tag (c) != AS_TAG_CATEGORY)
				continue;
			taken = as_node_take_data (c);
			if (taken == NULL)
				continue;
			g_ptr_array_add (priv->categories, taken);
		}
		break;

	/* <architectures> */
	case AS_TAG_ARCHITECTURES:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_ptr_array_set_size (priv->architectures, 0);
		for (c = n->children; c != NULL; c = c->next) {
			if (as_node_get_tag (c) != AS_TAG_ARCH)
				continue;
			taken = as_node_take_data (c);
			if (taken == NULL)
				continue;
			g_ptr_array_add (priv->architectures, taken);
		}
		break;

	/* <keywords> */
	case AS_TAG_KEYWORDS:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_hash_table_remove_all (priv->keywords);
		for (c = n->children; c != NULL; c = c->next) {
			if (as_node_get_tag (c) != AS_TAG_KEYWORD)
				continue;
			tmp = as_node_get_data (c);
			if (tmp == NULL)
				continue;
			taken = as_app_parse_locale (as_node_get_attribute (c, "xml:lang"));
			if (taken == NULL)
				continue;
			as_app_add_keyword (app, taken, tmp, -1);
			g_free (taken);
		}
		break;

	/* <kudos> */
	case AS_TAG_KUDOS:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_ptr_array_set_size (priv->kudos, 0);
		for (c = n->children; c != NULL; c = c->next) {
			if (as_node_get_tag (c) != AS_TAG_KUDO)
				continue;
			taken = as_node_take_data (c);
			if (taken == NULL)
				continue;
			g_ptr_array_add (priv->kudos, taken);
		}
		break;

	/* <vetos> */
	case AS_TAG_VETOS:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_ptr_array_set_size (priv->vetos, 0);
		for (c = n->children; c != NULL; c = c->next) {
			if (as_node_get_tag (c) != AS_TAG_VETO)
				continue;
			taken = as_node_take_data (c);
			if (taken == NULL)
				continue;
			g_ptr_array_add (priv->vetos, taken);
		}
		break;

	/* <mimetypes> */
	case AS_TAG_MIMETYPES:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_ptr_array_set_size (priv->mimetypes, 0);
		for (c = n->children; c != NULL; c = c->next) {
			if (as_node_get_tag (c) != AS_TAG_MIMETYPE)
				continue;
			taken = as_node_take_data (c);
			if (taken == NULL)
				continue;
			g_ptr_array_add (priv->mimetypes, taken);
		}
		break;

	/* <project_license> */
	case AS_TAG_PROJECT_LICENSE:
		g_free (priv->project_license);
		priv->project_license = as_node_take_data (n);
		break;

	/* <project_license> */
	case AS_TAG_METADATA_LICENSE:
		as_app_set_metadata_license (app, as_node_get_data (n), -1);
		break;

	/* <source_pkgname> */
	case AS_TAG_SOURCE_PKGNAME:
		as_app_set_source_pkgname (app, as_node_get_data (n), -1);
		break;

	/* <updatecontact> */
	case AS_TAG_UPDATE_CONTACT:
		as_app_set_update_contact (app, as_node_get_data (n), -1);
		break;

	/* <url> */
	case AS_TAG_URL:
		tmp = as_node_get_attribute (n, "type");
		as_app_add_url (app,
				as_url_kind_from_string (tmp),
				as_node_get_data (n), -1);
		break;

	/* <project_group> */
	case AS_TAG_PROJECT_GROUP:
		g_free (priv->project_group);
		priv->project_group = as_node_take_data (n);
		break;

	/* <compulsory_for_desktop> */
	case AS_TAG_COMPULSORY_FOR_DESKTOP:
		g_ptr_array_add (priv->compulsory_for_desktops,
				 as_node_take_data (n));
		break;

	/* <extends> */
	case AS_TAG_EXTENDS:
		g_ptr_array_add (priv->extends, as_node_take_data (n));
		break;

	/* <screenshots> */
	case AS_TAG_SCREENSHOTS:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_ptr_array_set_size (priv->screenshots, 0);
		for (c = n->children; c != NULL; c = c->next) {
			_cleanup_object_unref_ AsScreenshot *ss = NULL;
			if (as_node_get_tag (c) != AS_TAG_SCREENSHOT)
				continue;
			/* we don't yet support localised screenshots */
			if (as_node_get_attribute (c, "xml:lang") != NULL)
				continue;
			ss = as_screenshot_new ();
			if (!as_screenshot_node_parse (ss, c, error))
				return FALSE;
			as_app_add_screenshot (app, ss);
		}
		break;

	/* <releases> */
	case AS_TAG_RELEASES:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_ptr_array_set_size (priv->releases, 0);
		for (c = n->children; c != NULL; c = c->next) {
			_cleanup_object_unref_ AsRelease *r = NULL;
			if (as_node_get_tag (c) != AS_TAG_RELEASE)
				continue;
			r = as_release_new ();
			if (!as_release_node_parse (r, c, error))
				return FALSE;
			as_app_add_release (app, r);
		}
		break;

	/* <provides> */
	case AS_TAG_PROVIDES:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_ptr_array_set_size (priv->provides, 0);
		for (c = n->children; c != NULL; c = c->next) {
			_cleanup_object_unref_ AsProvide *p;
			p = as_provide_new ();
			if (!as_provide_node_parse (p, c, error))
				return FALSE;
			as_app_add_provide (app, p);
		}
		break;

	/* <languages> */
	case AS_TAG_LANGUAGES:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_hash_table_remove_all (priv->languages);
		for (c = n->children; c != NULL; c = c->next) {
			guint percent;
			if (as_node_get_tag (c) != AS_TAG_LANG)
				continue;
			percent = as_node_get_attribute_as_int (c, "percentage");
			if (percent == G_MAXINT)
				percent = 0;
			as_app_add_language (app, percent,
					     as_node_get_data (c), -1);
		}
		break;

	/* <metadata> */
	case AS_TAG_METADATA:
		if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA))
			g_hash_table_remove_all (priv->metadata);
		for (c = n->children; c != NULL; c = c->next) {
			AsKudoKind kudo;
			gchar *key;

			if (as_node_get_tag (c) != AS_TAG_VALUE)
				continue;
			key = as_node_take_attribute (c, "key");

			/* check if it's not an old-style metadata kudo */
			kudo = as_app_kudo_kind_from_legacy_string (key);
			if (kudo == AS_KUDO_KIND_UNKNOWN) {
				taken = as_node_take_data (c);
				if (taken == NULL)
					taken = g_strdup ("");
				g_hash_table_insert (priv->metadata, key, taken);
			} else {
				/* storing a a string is inelegant, but allows
				 * us to show kudos not (yet) supported */
				as_app_add_kudo_kind (app, kudo);
				g_free (key);
			}
		}
		break;
	default:
		break;
	}
	return TRUE;
}

/**
 * as_app_node_parse_full:
 **/
static gboolean
as_app_node_parse_full (AsApp *app, GNode *node, AsAppParseFlags flags, GError **error)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	GNode *n;
	const gchar *tmp;
	guint prio;

	/* new style */
	if (g_strcmp0 (as_node_get_name (node), "component") == 0) {
		tmp = as_node_get_attribute (node, "type");
		if (tmp != NULL)
			as_app_set_id_kind (app, as_id_kind_from_string (tmp));
		prio = as_node_get_attribute_as_int (node, "priority");
		if (prio != G_MAXINT && prio != 0)
			as_app_set_priority (app, prio);
	}

	/* parse each node */
	if (!(flags & AS_APP_PARSE_FLAG_APPEND_DATA)) {
		g_ptr_array_set_size (priv->compulsory_for_desktops, 0);
		g_ptr_array_set_size (priv->pkgnames, 0);
		g_ptr_array_set_size (priv->architectures, 0);
		g_ptr_array_set_size (priv->extends, 0);
		g_hash_table_remove_all (priv->keywords);
	}
	for (n = node->children; n != NULL; n = n->next) {
		if (!as_app_node_parse_child (app, n, flags, error))
			return FALSE;
	}
	return TRUE;
}

/**
 * as_app_node_parse:
 * @app: a #AsApp instance.
 * @node: a #GNode.
 * @error: A #GError or %NULL.
 *
 * Populates the object from a DOM node.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.1.0
 **/
gboolean
as_app_node_parse (AsApp *app, GNode *node, GError **error)
{
	return as_app_node_parse_full (app, node, AS_APP_PARSE_FLAG_NONE, error);
}

/**
 * as_app_node_parse_dep11:
 * @app: a #AsApp instance.
 * @node: a #GNode.
 * @error: A #GError or %NULL.
 *
 * Populates the object from a DEP-11 node.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.3.0
 **/
gboolean
as_app_node_parse_dep11 (AsApp *app, GNode *node, GError **error)
{
	GNode *c;
	GNode *c2;
	GNode *n;
	const gchar *tmp;

	for (n = node->children; n != NULL; n = n->next) {
		tmp = as_yaml_node_get_key (n);
		if (g_strcmp0 (tmp, "ID") == 0) {
			as_app_set_id (app, as_yaml_node_get_value (n), -1);
			continue;
		}
		if (g_strcmp0 (tmp, "Type") == 0) {
			if (g_strcmp0 (as_yaml_node_get_value (n), "desktop-app") == 0) {
				as_app_set_id_kind (app, AS_ID_KIND_DESKTOP);
				continue;
			}
			continue;
		}
		if (g_strcmp0 (tmp, "Packages") == 0) {
			for (c = n->children; c != NULL; c = c->next)
				as_app_add_pkgname (app, as_yaml_node_get_key (c), -1);
			continue;
		}
		if (g_strcmp0 (tmp, "Name") == 0) {
			for (c = n->children; c != NULL; c = c->next) {
				as_app_set_name (app,
						 as_yaml_node_get_key (c),
						 as_yaml_node_get_value (c), -1);
			}
			continue;
		}
		if (g_strcmp0 (tmp, "Summary") == 0) {
			for (c = n->children; c != NULL; c = c->next) {
				as_app_set_comment (app,
						    as_yaml_node_get_key (c),
						    as_yaml_node_get_value (c), -1);
			}
			continue;
		}
		if (g_strcmp0 (tmp, "Description") == 0) {
			for (c = n->children; c != NULL; c = c->next) {
				as_app_set_description (app,
							as_yaml_node_get_key (c),
							as_yaml_node_get_value (c), -1);
			}
			continue;
		}
		if (g_strcmp0 (tmp, "Keywords") == 0) {
			for (c = n->children; c != NULL; c = c->next) {
				for (c2 = c->children; c2 != NULL; c2 = c2->next) {
					if (as_yaml_node_get_key (c2) == NULL)
						continue;
					as_app_add_keyword (app,
							   as_yaml_node_get_key (c),
							   as_yaml_node_get_key (c2), -1);
				}
			}
			continue;
		}
		if (g_strcmp0 (tmp, "Categories") == 0) {
			for (c = n->children; c != NULL; c = c->next)
				as_app_add_category (app, as_yaml_node_get_key (c), -1);
			continue;
		}
		if (g_strcmp0 (tmp, "Icon") == 0) {
			for (c = n->children; c != NULL; c = c->next) {
				if (g_strcmp0 (as_yaml_node_get_key (c), "cached") == 0) {
					as_app_set_icon (app, as_yaml_node_get_value (c), -1);
					as_app_set_icon_kind (app, AS_ICON_KIND_CACHED);
					continue;
				}
			}
			continue;
		}
		if (g_strcmp0 (tmp, "Url") == 0) {
			for (c = n->children; c != NULL; c = c->next) {
				if (g_strcmp0 (as_yaml_node_get_key (c), "homepage") == 0) {
					as_app_add_url (app,
							AS_URL_KIND_HOMEPAGE,
							as_yaml_node_get_value (c),
							-1);
					continue;
				}
			}
			continue;
		}
		if (g_strcmp0 (tmp, "Provides") == 0) {
			for (c = n->children; c != NULL; c = c->next) {
				if (g_strcmp0 (as_yaml_node_get_key (c), "mimetypes") == 0) {
					for (c2 = c->children; c2 != NULL; c2 = c2->next) {
						as_app_add_mimetype (app,
								     as_yaml_node_get_key (c2),
								     -1);
					}
					continue;
				} else {
					_cleanup_object_unref_ AsProvide *pr = NULL;
					pr = as_provide_new ();
					if (!as_provide_node_parse_dep11 (pr, c, error))
						return FALSE;
					as_app_add_provide (app, pr);
				}
			}
			continue;
		}
		if (g_strcmp0 (tmp, "Screenshots") == 0) {
			for (c = n->children; c != NULL; c = c->next) {
				_cleanup_object_unref_ AsScreenshot *ss = NULL;
				ss = as_screenshot_new ();
				if (!as_screenshot_node_parse_dep11 (ss, c, error))
					return FALSE;
				as_app_add_screenshot (app, ss);
			}
			continue;
		}
	}
	return TRUE;
}

#if !GLIB_CHECK_VERSION(2,39,1)
/**
 * as_app_value_tokenize:
 **/
static gchar **
as_app_value_tokenize (const gchar *value)
{
	gchar *delim;
	gchar **tmp;
	gchar **values;
	guint i;
	delim = g_strdup (value);
	g_strdelimit (delim, "/,.;:", ' ');
	tmp = g_strsplit (delim, " ", -1);
	values = g_new0 (gchar *, g_strv_length (tmp) + 1);
	for (i = 0; tmp[i] != NULL; i++) {
		values[i] = g_utf8_strdown (tmp[i], -1);
	}
	g_strfreev (tmp);
	g_free (delim);
	return values;
}
#endif

/**
 * as_app_add_tokens:
 **/
static void
as_app_add_tokens (AsApp *app,
		   const gchar *value,
		   const gchar *locale,
		   guint score)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	AsAppTokenItem *token_item;

	/* sanity check */
	if (value == NULL) {
		g_critical ("trying to add NULL search token to %s",
			    as_app_get_id (app));
		return;
	}

	token_item = g_slice_new0 (AsAppTokenItem);
#if GLIB_CHECK_VERSION(2,39,1)
	token_item->values_utf8 = g_str_tokenize_and_fold (value,
							   locale,
							   &token_item->values_ascii);
#else
	token_item->values_utf8 = as_app_value_tokenize (value);
#endif
	token_item->score = score;
	g_ptr_array_add (priv->token_cache, token_item);
}

/**
 * as_app_create_token_cache_target:
 **/
static void
as_app_create_token_cache_target (AsApp *app, AsApp *donor)
{
	AsAppPrivate *priv = GET_PRIVATE (donor);
	GPtrArray *array;
	const gchar * const *locales;
	const gchar *tmp;
	guint i;
	guint j;

	/* add all the data we have */
	if (priv->id != NULL)
		as_app_add_tokens (app, priv->id, "C", 100);
	locales = g_get_language_names ();
	for (i = 0; locales[i] != NULL; i++) {
		if (g_str_has_suffix (locales[i], ".UTF-8"))
			continue;
		tmp = as_app_get_name (app, locales[i]);
		if (tmp != NULL)
			as_app_add_tokens (app, tmp, locales[i], 80);
		tmp = as_app_get_comment (app, locales[i]);
		if (tmp != NULL)
			as_app_add_tokens (app, tmp, locales[i], 60);
		tmp = as_app_get_description (app, locales[i]);
		if (tmp != NULL)
			as_app_add_tokens (app, tmp, locales[i], 20);
		array = as_app_get_keywords (app, locales[i]);
		if (array != NULL) {
			for (j = 0; j < array->len; j++) {
				tmp = g_ptr_array_index (array, j);
				as_app_add_tokens (app, tmp, locales[i], 90);
			}
		}
	}
	for (i = 0; i < priv->mimetypes->len; i++) {
		tmp = g_ptr_array_index (priv->mimetypes, i);
		as_app_add_tokens (app, tmp, "C", 1);
	}
}

/**
 * as_app_create_token_cache:
 **/
static void
as_app_create_token_cache (AsApp *app)
{
	AsApp *donor;
	AsAppPrivate *priv = GET_PRIVATE (app);
	guint i;

	as_app_create_token_cache_target (app, app);
	for (i = 0; i < priv->addons->len; i++) {
		donor = g_ptr_array_index (priv->addons, i);
		as_app_create_token_cache_target (app, donor);
	}
}

/**
 * as_app_search_matches:
 * @app: a #AsApp instance.
 * @search: the search term.
 *
 * Searches application data for a specific keyword.
 *
 * Returns: a match scrore, where 0 is no match and 100 is the best match.
 *
 * Since: 0.1.0
 **/
guint
as_app_search_matches (AsApp *app, const gchar *search)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	AsAppTokenItem *item;
	guint i, j;

	/* nothing to do */
	if (search == NULL)
		return 0;

	/* ensure the token cache is created */
	if (g_once_init_enter (&priv->token_cache_valid)) {
		as_app_create_token_cache (app);
		g_once_init_leave (&priv->token_cache_valid, TRUE);
	}

	/* find the search term */
	for (i = 0; i < priv->token_cache->len; i++) {
		item = g_ptr_array_index (priv->token_cache, i);

		/* prefer UTF-8 matches */
		if (item->values_utf8 != NULL) {
			for (j = 0; item->values_utf8[j] != NULL; j++) {
				if (g_str_has_prefix (item->values_utf8[j], search))
					return item->score;
			}
		}

		/* fall back to ASCII matches */
		if (item->values_ascii != NULL) {
			for (j = 0; item->values_ascii[j] != NULL; j++) {
				if (g_str_has_prefix (item->values_ascii[j], search))
					return item->score / 2;
			}
		}
	}
	return 0;
}

/**
 * as_app_search_matches_all:
 * @app: a #AsApp instance.
 * @search: the search terms.
 *
 * Searches application data for all the specific keywords.
 *
 * Returns: a match scrore, where 0 is no match and larger numbers are better
 * matches.
 *
 * Since: 0.1.3
 */
guint
as_app_search_matches_all (AsApp *app, gchar **search)
{
	guint i;
	guint matches_sum = 0;
	guint tmp;

	/* do *all* search keywords match */
	for (i = 0; search[i] != NULL; i++) {
		tmp = as_app_search_matches (app, search[i]);
		if (tmp == 0)
			return 0;
		matches_sum += tmp;
	}
	return matches_sum;
}

/**
 * as_app_desktop_key_get_locale:
 */
static gchar *
as_app_desktop_key_get_locale (const gchar *key)
{
	gchar *tmp1;
	gchar *tmp2;
	gchar *locale = NULL;

	tmp1 = g_strstr_len (key, -1, "[");
	if (tmp1 == NULL)
		return NULL;
	tmp2 = g_strstr_len (tmp1, -1, "]");
	if (tmp1 == NULL)
		return NULL;
	locale = g_strdup (tmp1 + 1);
	locale[tmp2 - tmp1 - 1] = '\0';
	return locale;
}

/**
 * as_app_infer_file_key:
 **/
static gboolean
as_app_infer_file_key (AsApp *app,
		       GKeyFile *kf,
		       const gchar *key,
		       GError **error)
{
	_cleanup_free_ gchar *tmp = NULL;

	if (g_strcmp0 (key, "X-GNOME-UsesNotifications") == 0) {
		as_app_add_kudo_kind (AS_APP (app),
				      AS_KUDO_KIND_NOTIFICATIONS);

	} else if (g_strcmp0 (key, "X-GNOME-Bugzilla-Product") == 0) {
		as_app_set_project_group (app, "GNOME", -1);

	} else if (g_strcmp0 (key, "X-MATE-Bugzilla-Product") == 0) {
		as_app_set_project_group (app, "MATE", -1);

	} else if (g_strcmp0 (key, "X-KDE-StartupNotify") == 0) {
		as_app_set_project_group (app, "KDE", -1);

	} else if (g_strcmp0 (key, "X-DocPath") == 0) {
		tmp = g_key_file_get_string (kf,
					     G_KEY_FILE_DESKTOP_GROUP,
					     key,
					     NULL);
		if (g_str_has_prefix (tmp, "http://userbase.kde.org/"))
			as_app_set_project_group (app, "KDE", -1);

	/* Exec */
	} else if (g_strcmp0 (key, G_KEY_FILE_DESKTOP_KEY_EXEC) == 0) {
		tmp = g_key_file_get_string (kf,
					     G_KEY_FILE_DESKTOP_GROUP,
					     key,
					     NULL);
		if (g_str_has_prefix (tmp, "xfce4-"))
			as_app_set_project_group (app, "XFCE", -1);
	}

	return TRUE;
}

/**
 * as_app_parse_file_key:
 **/
static gboolean
as_app_parse_file_key (AsApp *app,
		       GKeyFile *kf,
		       const gchar *key,
		       GError **error)
{
	gchar *dot = NULL;
	guint i;
	guint j;
	_cleanup_free_ gchar *locale = NULL;
	_cleanup_free_ gchar *tmp = NULL;
	_cleanup_strv_free_ gchar **list = NULL;

	/* NoDisplay */
	if (g_strcmp0 (key, G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY) == 0) {
		as_app_add_veto (app, "NoDisplay=true");

	/* Type */
	} else if (g_strcmp0 (key, G_KEY_FILE_DESKTOP_KEY_TYPE) == 0) {
		tmp = g_key_file_get_string (kf,
					     G_KEY_FILE_DESKTOP_GROUP,
					     key,
					     NULL);
		if (g_strcmp0 (tmp, G_KEY_FILE_DESKTOP_TYPE_APPLICATION) != 0) {
			g_set_error_literal (error,
					     AS_APP_ERROR,
					     AS_APP_ERROR_INVALID_TYPE,
					     "not an application");
			return FALSE;
		}

	/* Icon */
	} else if (g_strcmp0 (key, G_KEY_FILE_DESKTOP_KEY_ICON) == 0) {
		tmp = g_key_file_get_string (kf,
					     G_KEY_FILE_DESKTOP_GROUP,
					     key,
					     NULL);
		if (tmp != NULL && tmp[0] != '\0') {
			as_app_set_icon (app, tmp, -1);
			dot = g_strstr_len (tmp, -1, ".");
			if (dot != NULL)
				*dot = '\0';
			if (as_utils_is_stock_icon_name (tmp)) {
				as_app_set_icon (app, tmp, -1);
				as_app_set_icon_kind (app, AS_ICON_KIND_STOCK);
			}
		}

	/* Categories */
	} else if (g_strcmp0 (key, G_KEY_FILE_DESKTOP_KEY_CATEGORIES) == 0) {
		list = g_key_file_get_string_list (kf,
						   G_KEY_FILE_DESKTOP_GROUP,
						   key,
						   NULL, NULL);
		for (i = 0; list[i] != NULL; i++) {

			/* check categories that if present would blacklist
			 * the application */
			if (fnmatch ("X-*-Settings-Panel", list[i], 0) == 0 ||
			    fnmatch ("X-*-Settings", list[i], 0) == 0 ||
			    fnmatch ("X-*-SettingsDialog", list[i], 0) == 0) {
				as_app_add_veto (app, "category '%s' blacklisted", list[i]);
				continue;
			}

			/* not a standard category */
			if (g_str_has_prefix (list[i], "X-"))
				continue;

			/* check the category is valid */
			if (!as_utils_is_category_id (list[i]))
				continue;

			/* ignore some useless keys */
			if (g_strcmp0 (list[i], "GTK") == 0)
				continue;
			if (g_strcmp0 (list[i], "Qt") == 0)
				continue;
			if (g_strcmp0 (list[i], "KDE") == 0)
				continue;
			if (g_strcmp0 (list[i], "GNOME") == 0)
				continue;
			as_app_add_category (app, list[i], -1);
		}

	} else if (g_strcmp0 (key, "Keywords") == 0) {
		list = g_key_file_get_string_list (kf,
						   G_KEY_FILE_DESKTOP_GROUP,
						   key,
						   NULL, NULL);
		for (i = 0; list[i] != NULL; i++) {
			_cleanup_strv_free_ gchar **kw_split = NULL;
			kw_split = g_strsplit (list[i], ",", -1);
			for (j = 0; kw_split[j] != NULL; j++) {
				if (kw_split[j][0] == '\0')
					continue;
				as_app_add_keyword (app, "C",
						    kw_split[j], -1);
			}
		}

	} else if (g_str_has_prefix (key, "Keywords")) {
		locale = as_app_desktop_key_get_locale (key);
		list = g_key_file_get_locale_string_list (kf,
							  G_KEY_FILE_DESKTOP_GROUP,
							  key,
							  locale,
							  NULL, NULL);
		for (i = 0; list[i] != NULL; i++) {
			_cleanup_strv_free_ gchar **kw_split = NULL;
			kw_split = g_strsplit (list[i], ",", -1);
			for (j = 0; kw_split[j] != NULL; j++) {
				if (kw_split[j][0] == '\0')
					continue;
				as_app_add_keyword (app, locale,
						    kw_split[j], -1);
			}
		}

	} else if (g_strcmp0 (key, "MimeType") == 0) {
		list = g_key_file_get_string_list (kf,
						   G_KEY_FILE_DESKTOP_GROUP,
						   key,
						   NULL, NULL);
		for (i = 0; list[i] != NULL; i++)
			as_app_add_mimetype (app, list[i], -1);

	} else if (g_strcmp0 (key, "X-AppInstall-Package") == 0) {
		tmp = g_key_file_get_string (kf,
					     G_KEY_FILE_DESKTOP_GROUP,
					     key,
					     NULL);
		if (tmp != NULL && tmp[0] != '\0')
			as_app_add_pkgname (app, tmp, -1);

	/* OnlyShowIn */
	} else if (g_strcmp0 (key, G_KEY_FILE_DESKTOP_KEY_ONLY_SHOW_IN) == 0) {
		/* if an app has only one entry, it's that desktop */
		list = g_key_file_get_string_list (kf,
						   G_KEY_FILE_DESKTOP_GROUP,
						   key,
						   NULL, NULL);
		if (g_strv_length (list) == 1)
			as_app_set_project_group (app, list[0], -1);

	/* Name */
	} else if (g_strcmp0 (key, G_KEY_FILE_DESKTOP_KEY_NAME) == 0 ||
	           g_strcmp0 (key, "_Name") == 0) {
		tmp = g_key_file_get_string (kf,
					     G_KEY_FILE_DESKTOP_GROUP,
					     key,
					     NULL);
		if (tmp != NULL && tmp[0] != '\0')
			as_app_set_name (app, "C", tmp, -1);

	/* Name[] */
	} else if (g_str_has_prefix (key, G_KEY_FILE_DESKTOP_KEY_NAME)) {
		locale = as_app_desktop_key_get_locale (key);
		tmp = g_key_file_get_locale_string (kf,
						    G_KEY_FILE_DESKTOP_GROUP,
						    G_KEY_FILE_DESKTOP_KEY_NAME,
						    locale,
						    NULL);
		if (tmp != NULL && tmp[0] != '\0')
			as_app_set_name (app, locale, tmp, -1);

	/* Comment */
	} else if (g_strcmp0 (key, G_KEY_FILE_DESKTOP_KEY_COMMENT) == 0 ||
	           g_strcmp0 (key, "_Comment") == 0) {
		tmp = g_key_file_get_string (kf,
					     G_KEY_FILE_DESKTOP_GROUP,
					     key,
					     NULL);
		if (tmp != NULL && tmp[0] != '\0')
			as_app_set_comment (app, "C", tmp, -1);

	/* Comment[] */
	} else if (g_str_has_prefix (key, G_KEY_FILE_DESKTOP_KEY_COMMENT)) {
		locale = as_app_desktop_key_get_locale (key);
		tmp = g_key_file_get_locale_string (kf,
						    G_KEY_FILE_DESKTOP_GROUP,
						    G_KEY_FILE_DESKTOP_KEY_COMMENT,
						    locale,
						    NULL);
		if (tmp != NULL && tmp[0] != '\0')
			as_app_set_comment (app, locale, tmp, -1);

	/* non-standard */
	} else if (g_strcmp0 (key, "X-Ubuntu-Software-Center-Name") == 0) {
		tmp = g_key_file_get_string (kf,
					     G_KEY_FILE_DESKTOP_GROUP,
					     key,
					     NULL);
		if (tmp != NULL && tmp[0] != '\0')
			as_app_set_name (app, "C", tmp, -1);
	} else if (g_str_has_prefix (key, "X-Ubuntu-Software-Center-Name")) {
		locale = as_app_desktop_key_get_locale (key);
		tmp = g_key_file_get_locale_string (kf,
						    G_KEY_FILE_DESKTOP_GROUP,
						    "X-Ubuntu-Software-Center-Name",
						    locale,
						    NULL);
		if (tmp != NULL && tmp[0] != '\0')
			as_app_set_name (app, locale, tmp, -1);
	}

	return TRUE;
}

/**
 * as_app_parse_desktop_file:
 **/
static gboolean
as_app_parse_desktop_file (AsApp *app,
			   const gchar *desktop_file,
			   AsAppParseFlags flags,
			   GError **error)
{
	GKeyFileFlags kf_flags = G_KEY_FILE_KEEP_TRANSLATIONS;
	gchar *tmp;
	guint i;
	_cleanup_free_ gchar *app_id = NULL;
	_cleanup_keyfile_unref_ GKeyFile *kf = NULL;
	_cleanup_strv_free_ gchar **keys = NULL;

	/* load file */
	kf = g_key_file_new ();
	if (flags & AS_APP_PARSE_FLAG_KEEP_COMMENTS)
		kf_flags |= G_KEY_FILE_KEEP_COMMENTS;
	if (!g_key_file_load_from_file (kf, desktop_file, kf_flags, error))
		return FALSE;

	/* create app */
	app_id = g_path_get_basename (desktop_file);
	as_app_set_id_kind (app, AS_ID_KIND_DESKTOP);

	/* is blacklisted */
	if (as_utils_is_blacklisted_id (app_id))
		as_app_add_veto (app, "%s is not an application", app_id);

	/* Ubuntu helpfully put the package name in the desktop file name */
	tmp = g_strstr_len (app_id, -1, ":");
	if (tmp != NULL)
		as_app_set_id (app, tmp + 1, -1);
	else
		as_app_set_id (app, app_id, -1);

	/* look at all the keys */
	keys = g_key_file_get_keys (kf, G_KEY_FILE_DESKTOP_GROUP, NULL, error);
	if (keys == NULL)
		return FALSE;
	for (i = 0; keys[i] != NULL; i++) {
		if (!as_app_parse_file_key (app, kf, keys[i], error))
			return FALSE;
		if ((flags & AS_APP_PARSE_FLAG_USE_HEURISTICS) > 0) {
			if (!as_app_infer_file_key (app, kf, keys[i], error))
				return FALSE;
		}
	}

	/* all applications require icons */
	if (as_app_get_icon (app) == NULL)
		as_app_add_veto (app, "%s has no icon", app_id);

	return TRUE;
}

/**
 * as_app_parse_appdata_unintltoolize_cb:
 **/
static gboolean
as_app_parse_appdata_unintltoolize_cb (GNode *node, gpointer data)
{
	AsAppPrivate *priv = GET_PRIVATE (AS_APP (data));
	const gchar *name;

	name = as_node_get_name (node);
	if (g_strcmp0 (name, "_name") == 0) {
		as_node_set_name (node, "name");
		priv->problems |= AS_APP_PROBLEM_INTLTOOL_NAME;
		return FALSE;
	}
	if (g_strcmp0 (name, "_summary") == 0) {
		as_node_set_name (node, "summary");
		priv->problems |= AS_APP_PROBLEM_INTLTOOL_SUMMARY;
		return FALSE;
	}
	if (g_strcmp0 (name, "_caption") == 0) {
		as_node_set_name (node, "caption");
		return FALSE;
	}
	if (g_strcmp0 (name, "_p") == 0) {
		as_node_set_name (node, "p");
		priv->problems |= AS_APP_PROBLEM_INTLTOOL_DESCRIPTION;
		return FALSE;
	}
	if (g_strcmp0 (name, "_li") == 0) {
		as_node_set_name (node, "li");
		priv->problems |= AS_APP_PROBLEM_INTLTOOL_DESCRIPTION;
		return FALSE;
	}
	if (g_strcmp0 (name, "_ul") == 0) {
		as_node_set_name (node, "ul");
		priv->problems |= AS_APP_PROBLEM_INTLTOOL_DESCRIPTION;
		return FALSE;
	}
	if (g_strcmp0 (name, "_ol") == 0) {
		as_node_set_name (node, "ol");
		priv->problems |= AS_APP_PROBLEM_INTLTOOL_DESCRIPTION;
		return FALSE;
	}
	return FALSE;
}

/**
 * as_app_parse_appdata_file:
 **/
static gboolean
as_app_parse_appdata_file (AsApp *app,
			   const gchar *filename,
			   AsAppParseFlags flags,
			   GError **error)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	AsNodeFromXmlFlags from_xml_flags = AS_NODE_FROM_XML_FLAG_NONE;
	GNode *l;
	GNode *node;
	gboolean seen_application = FALSE;
	gchar *tmp;
	gsize len;
	_cleanup_free_ gchar *data = NULL;
	_cleanup_node_unref_ GNode *root = NULL;

	/* open file */
	if (!g_file_get_contents (filename, &data, &len, error))
		return FALSE;

	/* validate */
	tmp = g_strstr_len (data, len, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
	if (tmp == NULL)
		tmp = g_strstr_len (data, len, "<?xml version=\"1.0\" encoding=\"utf-8\"?>");
	if (tmp == NULL)
		priv->problems |= AS_APP_PROBLEM_NO_XML_HEADER;

	/* check for copyright */
	tmp = g_strstr_len (data, len, "<!-- Copyright");
	if (tmp == NULL)
		priv->problems |= AS_APP_PROBLEM_NO_COPYRIGHT_INFO;

	/* parse */
	if (flags & AS_APP_PARSE_FLAG_KEEP_COMMENTS)
		from_xml_flags |= AS_NODE_FROM_XML_FLAG_KEEP_COMMENTS;
	root = as_node_from_xml (data, len,
				 from_xml_flags,
				 error);
	if (root == NULL) {
		g_prefix_error (error,
				"failed to parse '%s': ",
				filename);
		return FALSE;
	}

	/* make the <_summary> tags into <summary> */
	if (flags & AS_APP_PARSE_FLAG_CONVERT_TRANSLATABLE) {
		g_node_traverse (root,
				 G_IN_ORDER,
				 G_TRAVERSE_ALL,
				 10,
				 as_app_parse_appdata_unintltoolize_cb,
				 app);
	}

	node = as_node_find (root, "application");
	if (node == NULL)
		node = as_node_find (root, "component");
	if (node == NULL) {
		g_set_error (error,
			     AS_APP_ERROR,
			     AS_APP_ERROR_INVALID_TYPE,
			     "%s has an unrecognised contents",
			     filename);
		return FALSE;
	}
	for (l = node->children; l != NULL; l = l->next) {
		if (g_strcmp0 (as_node_get_name (l), "licence") == 0 ||
		    g_strcmp0 (as_node_get_name (l), "license") == 0) {
			as_node_set_name (l, "metadata_license");
			priv->problems |= AS_APP_PROBLEM_DEPRECATED_LICENCE;
			continue;
		}
		if (as_node_get_tag (l) == AS_TAG_APPLICATION) {
			if (seen_application)
				priv->problems |= AS_APP_PROBLEM_MULTIPLE_ENTRIES;
			seen_application = TRUE;
		}
	}
	if (!as_app_node_parse_full (app, node, flags, error))
		return FALSE;
	return TRUE;
}

/**
 * as_app_parse_file:
 * @app: a #AsApp instance.
 * @filename: file to load.
 * @flags: #AsAppParseFlags, e.g. %AS_APP_PARSE_FLAG_USE_HEURISTICS
 * @error: A #GError or %NULL.
 *
 * Parses a desktop or AppData file and populates the application state.
 *
 * Applications that are not suitable for the store will have vetos added.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.1.2
 **/
gboolean
as_app_parse_file (AsApp *app,
		   const gchar *filename,
		   AsAppParseFlags flags,
		   GError **error)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	GPtrArray *vetos;

	/* autodetect */
	if (priv->source_kind == AS_APP_SOURCE_KIND_UNKNOWN) {
		priv->source_kind = as_app_guess_source_kind (filename);
		if (priv->source_kind == AS_APP_SOURCE_KIND_UNKNOWN) {
			g_set_error (error,
				     AS_APP_ERROR,
				     AS_APP_ERROR_INVALID_TYPE,
				     "%s has an unrecognised extension",
				     filename);
			return FALSE;
		}
	}

	/* convert <_p> into <p> for easy validation */
	if (g_str_has_suffix (filename, ".appdata.xml.in") ||
	    g_str_has_suffix (filename, ".metainfo.xml.in"))
		flags |= AS_APP_PARSE_FLAG_CONVERT_TRANSLATABLE;

	/* all untrusted */
	as_app_set_trust_flags (AS_APP (app),
				AS_APP_TRUST_FLAG_CHECK_DUPLICATES |
				AS_APP_TRUST_FLAG_CHECK_VALID_UTF8);

	/* set the source location */
	as_app_set_source_file (app, filename);

	/* parse */
	switch (priv->source_kind) {
	case AS_APP_SOURCE_KIND_DESKTOP:
		if (!as_app_parse_desktop_file (app, filename, flags, error))
			return FALSE;
		break;
	case AS_APP_SOURCE_KIND_APPDATA:
	case AS_APP_SOURCE_KIND_METAINFO:
		if (!as_app_parse_appdata_file (app, filename, flags, error))
			return FALSE;
		break;
	default:
		g_set_error (error,
			     AS_APP_ERROR,
			     AS_APP_ERROR_INVALID_TYPE,
			     "%s has an unhandled type",
			     filename);
		return FALSE;
		break;
	}

	/* vetos are errors by default */
	vetos = as_app_get_vetos (app);
	if ((flags & AS_APP_PARSE_FLAG_ALLOW_VETO) == 0 && vetos->len > 0) {
		const gchar *tmp = g_ptr_array_index (vetos, 0);
		g_set_error_literal (error,
				     AS_APP_ERROR,
				     AS_APP_ERROR_INVALID_TYPE,
				     tmp);
		return FALSE;
	}

	return TRUE;
}

/**
 * as_app_to_file:
 * @app: a #AsApp instance.
 * @file: a #GFile
 * @cancellable: A #GCancellable, or %NULL
 * @error: A #GError or %NULL
 *
 * Exports a DOM tree to an XML file.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.2.0
 **/
gboolean
as_app_to_file (AsApp *app,
		GFile *file,
		GCancellable *cancellable,
		GError **error)
{
	_cleanup_node_unref_ GNode *root = NULL;
	_cleanup_string_free_ GString *xml = NULL;

	root = as_node_new ();
	as_app_node_insert (app, root, 1.0);
	xml = as_node_to_xml (root,
			      AS_NODE_TO_XML_FLAG_ADD_HEADER |
			      AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
			      AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE);
	return g_file_replace_contents (file,
					xml->str,
					xml->len,
					NULL,
					FALSE,
					G_FILE_CREATE_NONE,
					NULL,
					cancellable,
					error);
}

/**
 * as_app_get_vetos:
 * @app: A #AsApp
 *
 * Gets the list of vetos.
 *
 * Returns: (transfer none) (element-type utf8): A list of vetos
 *
 * Since: 0.2.5
 **/
GPtrArray *
as_app_get_vetos (AsApp *app)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	return priv->vetos;
}

/**
 * as_app_add_veto:
 * @app: A #AsApp
 * @fmt: format string
 * @...: varargs
 *
 * Adds a reason to not include the application in the metadata.
 *
 * Since: 0.2.5
 **/
void
as_app_add_veto (AsApp *app, const gchar *fmt, ...)
{
	AsAppPrivate *priv = GET_PRIVATE (app);
	gchar *tmp;
	va_list args;
	va_start (args, fmt);
	tmp = g_strdup_vprintf (fmt, args);
	va_end (args);
	g_ptr_array_add (priv->vetos, tmp);
}

/**
 * as_app_new:
 *
 * Creates a new #AsApp.
 *
 * Returns: (transfer full): a #AsApp
 *
 * Since: 0.1.0
 **/
AsApp *
as_app_new (void)
{
	AsApp *app;
	app = g_object_new (AS_TYPE_APP, NULL);
	return AS_APP (app);
}
